Skip to main content

Hooks

Hooks are per-instance lifecycle methods you override on your model class.

Available Hooks

class Order(Model):
total = DecimalField(max_digits=10, decimal_places=2)
status = CharField(max_length=20, default="pending")

async def clean(self) -> None:
"""Cross-field validation. Called by full_clean()."""
if self.total <= 0:
raise ValidationError({"total": ["Total must be positive"]})

async def before_save(self, created: bool) -> None:
"""Called before INSERT (created=True) or UPDATE (created=False)."""
if created:
self.status = "pending"

async def after_save(self, created: bool) -> None:
"""Called after INSERT or UPDATE."""
if created:
await notify_user(self.user_id, "Order created")

async def before_delete(self) -> None:
"""Called before DELETE. Raise to prevent deletion."""
if self.status == "shipped":
raise ValueError("Cannot delete a shipped order")

async def after_delete(self) -> None:
"""Called after DELETE."""
await notify_user(self.user_id, "Order cancelled")

Hooks vs Signals

HooksSignals
ScopePer-instanceGlobal
DefinitionOverride on modelConnect anywhere
CouplingTight (in model)Loose (decoupled)
Use forModel-specific logicCross-cutting concerns

When to Use Hooks

  • Setting default values based on other fields
  • Model-specific validation
  • Triggering actions tied to this specific model

When to Use Signals

  • Logging/auditing across many models
  • Cache invalidation
  • Notifications that involve multiple systems
  • Decoupled side effects

Execution Order

save()
→ full_clean()
→ field validators
→ clean()
→ before_save(created)
→ pre_save signal
→ SQL INSERT/UPDATE
→ post_save signal
→ after_save(created)
warning

Bulk operations (bulk_create, bulk_update, bulk_delete, .update(), .delete()) bypass per-instance hooks. Use signals for bulk-aware logic.

Next Steps

Caching — Query result caching