CRUD Operations
Create, Read, Update, Delete - the four operations every data layer performs - are in Peewee a small set of classmethods on the model: create, select/get, save/update, delete. The API is uniform and the SQL is simple, which is the ORM's daily work and the part of the tutorial you will use every day. This chapter covers each operation with the blog domain, the idioms that keep the writes correct (create vs insert, save vs update), and the read patterns (get, get_or_create, the query that returns a list).
Create: Inserting a Row
from peewee import *
# create(): insert and return the instance (the common case)
a = Author.create(name="Ada", email="[email protected]", bio="Engineer")
print(a.id) # the new primary key
print(a.created_at) # the default was applied
# create with an explicit id (when you manage the key)
b = Author.create(id=99, name="Bob", email="[email protected]")
# the lower-level insert(): batch and options, returns a query result
# insert_many for a list of dicts (the bulk chapter covers it in depth)
Author.insert_many([
{"name": "C1", "email": "[email protected]"},
{"name": "C2", "email": "[email protected]"},
]).execute()
# get_or_create(): the "insert if absent" idiom
inst, created = Author.get_or_create(
email="[email protected]",
defaults={"name": "Ada"},
)
print(created) # False - it already existed
create() is the everyday insert: it issues the INSERT, applies the field defaults, and returns the instance with its new id - the method to reach for in almost every write. The distinction to know: create is the convenience that returns an instance, while insert()/insert_many() are the lower-level query builders that give you the batch and the options (on_conflict for an upsert) and return a result count instead of an instance. get_or_create is the "I need this row to exist" idiom - it queries by the given fields and creates with the defaults if absent, returning the instance and a created flag. The upsert (insert(...).on_conflict(...)) is the "insert or update on conflict" for the case where the row may exist and should be updated - the bulk chapter covers the full form.
Read: select, get, and the List
# select(): a query over all (or a filtered) rows - returns a query
all_authors = Author.select().execute() # a list of instances
for a in all_authors:
print(a.name)
# .get(): exactly one row, or an error
# DoesNotExist if zero, MultipleResults if more than one
ada = Author.get(Author.email == "[email protected]")
print(ada.name)
# .get_or_none(): one row or None - the no-exception form
maybe = Author.get_or_none(Author.email == "[email protected]")
print(maybe) # None
# .first(): the first row or None - the cheap "is there any"
any_author = Author.select().first()
# the query is lazy - .execute() is what runs the SQL
q = Author.select().where(Author.is_active == True)
print(q) # the SQL, not yet run
rows = q.execute() # now it runs
print(len(rows))
The read API is select() (a query over rows), get() (exactly one, with DoesNotExist/MultipleResults as the error contract), and get_or_none()/first() (the no-exception single-row forms). The query is lazy - building Author.select().where(...) constructs the expression, and .execute() runs the SQL and returns the instances. The get error contract is the one to internalize: get raises DoesNotExist on zero rows and MultipleResults on more than one, which is the ORM telling you the query was not unique - catch DoesNotExist for the "not found" path, or use get_or_none when None is the expected absence. The print(q) showing the SQL is the inspection habit - the query object renders its SQL, so you can see exactly what will run before you run it.
Update: Mutating a Row
# save(): persist the instance's current field values
a = Author.get(Author.email == "[email protected]")
a.bio = "Updated bio"
a.is_active = False
a.save() # UPDATE authors SET ... WHERE id = a.id
# save with a field subset: only these columns are written
a.name = "Ada L."
a.save(only=[Author.name]) # UPDATE only the name column
# the classmethod update(): a query-level update, no instance needed
# update all inactive authors' email domain
(Author.update(is_active=True)
.where(Author.email ** "@old.example.com"))
.execute() # returns the row count
# increment without a read-modify-write race:
Author.update(views=Article.views + 1).where(Author.id == a.id)
.execute()
Two update paths. save() persists the instance's current values - the "I changed the object, now write it" form, with the only=[...] argument to restrict the columns written (the safe form when other code may have changed other columns). The classmethod update() is the query-level form - Author.update(field=value).where(...).execute() - which issues the UPDATE ... WHERE directly without loading an instance, and returns the affected row count. The update expression is where the increment idiom lives: views=Article.views + 1 is a column arithmetic in the SQL (SET views = views + 1), not a Python read-modify-write, which is the race-free form for counters. The choice is the same as the create choice - save when you have the instance and changed it, update() when the change is a condition over rows.
Delete: Removing Rows
# delete the instance
a = Author.get(Author.email == "[email protected]")
a.delete_instance() # DELETE FROM authors WHERE id = a.id
a.delete_instance(recursive=True) # also delete related rows (the FKs)
# the classmethod delete(): a query-level delete
deleted = (Article.delete()
.where(Article.published == False))
.execute()
print(deleted) # the number of rows removed
# delete_instance(recursive=True) follows the ForeignKeyFields and
# removes the related rows first - the ORM's cascade, explicit
delete_instance() removes the instance's row, and the recursive=True argument follows the model's foreign keys to delete the related rows first - the explicit cascade, the ORM's answer to the database's ON DELETE behavior (which the field's on_delete argument also controls at the schema level). The classmethod delete() is the query-level form - Article.delete().where(...).execute() - which removes the matching rows and returns the count, the same shape as the update() classmethod. The delete is the operation to be most deliberate about: a delete() without a where removes every row in the table, which is why the query-level delete is always written with its condition visible, and the recursive flag is the one that makes a delete reach the related data.
The Working Idioms
createfor a single insert with an instance back,insert_manyfor a batch,get_or_createfor the "must exist" row, and theon_conflictupsert for the insert-or-update.getwhen exactly one is expected (with theDoesNotExistcatch for the absence),get_or_none/firstwhenNoneis a normal answer, andselect()for the list.savefor the instance you changed,update()for the condition over rows, and the column arithmetic (field = field + 1) for the race-free counter.- The query is lazy - build it, inspect it with
print(q), then.execute(); the SQL you see is the SQL that runs. - Always write the
whereon a query-levelupdate/delete- an unconditioned one touches the whole table.
Summary
The CRUD is a small, uniform API: create() inserts and returns the instance (with insert_many for batches and get_or_create for the must-exist row), select()/get()/get_or_none() read (with get's DoesNotExist/MultipleResults contract and the lazy query that .execute() runs), save()/update() write (the instance form and the query form, with the column arithmetic for the race-free counter), and delete_instance()/delete() remove (with the recursive flag for the explicit cascade). The idioms are the discipline: create vs insert, save vs update, the always-present where on a query-level update or delete, and the print(q) inspection that keeps the generated SQL visible. These are the operations the app performs daily, and the next chapter is the expression language that makes the where clauses - the queries - as expressive as the writes.