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โ
| Hooks | Signals | |
|---|---|---|
| Scope | Per-instance | Global |
| Definition | Override on model | Connect anywhere |
| Coupling | Tight (in model) | Loose (decoupled) |
| Use for | Model-specific logic | Cross-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