Signals¶
Signals are lifecycle hooks that let you run code around a model's persistence operations. yara_orm ships four signals — pre_save, post_save, pre_delete and post_delete — so you can react to inserts, updates and deletes without scattering logic across your call sites. Because this is an async Python ORM, every handler is a coroutine that is awaited inline during the save() or delete() it belongs to.
Each signal is a decorator imported from yara_orm that takes the model class it should fire for:
Handler signatures¶
The signatures differ per signal. Get them exactly right — handlers are called positionally.
| Signal | Async handler signature |
|---|---|
pre_save |
async def handler(sender, instance, using_db, update_fields) |
post_save |
async def handler(sender, instance, created, using_db, update_fields) |
pre_delete |
async def handler(sender, instance, using_db) |
post_delete |
async def handler(sender, instance, using_db) |
Parameters¶
sender— the model class the signal was registered for (the same class you passed to the decorator).instance— the model instance being saved or deleted. Inpre_saveyou may still mutate it before it is written; changes are persisted.created— (post_save only) aboolthat isTruewhen the save was an INSERT (a brand-new row) andFalsewhen it was an UPDATE.using_db— the database executor used for the operation. It is the same executor that runs the underlying SQL, so you can issue queries on the same connection.update_fields—list[str] | None. The list of field names passed tosave(update_fields=...), orNonefor a full save. It is informational, passed straight through to your handler.
Handlers are async and awaited
Every handler is a coroutine and is awaited as part of the save()/delete() call. pre_* handlers run before the SQL statement; post_* handlers run after it. You can register multiple handlers for the same signal on the same model — they run in registration order.
Mutating in pre_save¶
A pre_save handler can change the instance before it is written. For example, deriving a slug:
from yara_orm import pre_save
@pre_save(Author)
async def fill_slug(sender, instance, using_db, update_fields):
instance.slug = instance.name.lower()
The mutation happens before the INSERT/UPDATE, so the new value is what gets stored.
Reacting to create vs. update with post_save¶
The created flag lets a single post_save handler branch between first insert and subsequent updates:
from yara_orm import post_save
@post_save(Book)
async def on_book_saved(sender, instance, created, using_db, update_fields):
if created:
# Runs once, when the row is first inserted.
await send_new_book_notification(instance)
else:
# Runs on every later update.
await reindex_book(instance)
book = await Book.create(title="Dune") # post_save fires with created=True
book.title = "Dune (Special Edition)"
await book.save() # post_save fires with created=False
When signals fire¶
Signals fire for instance-level operations:
instance.save()emitspre_savethenpost_save.Model.create(...)callssave()internally, so it emits the save signals too (withcreated=True).instance.delete()emitspre_deletethenpost_delete.
Bulk queryset operations bypass signals
QuerySet-level bulk methods run a single SQL statement and do not load or instantiate rows, so they emit no signals:
Model.filter(...).update(...)Model.filter(...).delete()Model.bulk_create(...)
If you need per-row hooks to run, iterate and call instance.save() / instance.delete() on each object instead of using the bulk path.
Registration is global
Decorating a handler registers it for the process. Register your handlers at import time (e.g. in a module that is imported during app startup) so they are in place before any save()/delete() runs.