Model Hooks¶
Hooks let you run custom logic at specific points in a model's lifecycle — before or after creating, updating, or deleting objects.
Warning
Do not call .save() or .delete() on the same object (self) inside a hook — this causes infinite recursion. Calling these methods on other model instances is fine.
Initialization Hooks¶
pre_init¶
Called before the model is initialized and Pydantic validation runs. Use it to set default values or transform input data.
The kwargs dict contains the constructor arguments — modify it to change what gets passed to the model.
from typing import Any
def pre_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None:
if is_new_object:
kwargs['name'] = kwargs.get('name', 'Default Name')
# Set a custom object ID based on another field
if kwargs.get('custom_id_field'):
kwargs['_object_id'] = kwargs['custom_id_field']
Note
The Pydantic object is not fully initialized at this point. Access fields through kwargs, not self.
post_init¶
Called after initialization and validation. The model instance is fully constructed.
from typing import Any
def post_init(self, *, is_new_object: bool, kwargs: dict[str, Any]) -> None:
if self.name.islower():
msg = 'Name must not be entirely lowercase'
raise ValueError(msg)
Lifecycle Hooks¶
All lifecycle hooks accept only self. Each has a sync and async variant:
| Event | Sync | Async |
|---|---|---|
| Before create | pre_create |
apre_create |
| After create | post_create |
apost_create |
| Before update | pre_update |
apre_update |
| After update | post_update |
apost_update |
| Before delete | pre_delete |
apre_delete |
| After delete | post_delete |
apost_delete |
pre_create / apre_create¶
Called before a new object is saved to the database for the first time.
def pre_create(self) -> None:
if not self.name:
self.name = 'Default Name'
async def apre_create(self) -> None:
if not self.name:
self.name = 'Default Name'
post_create / apost_create¶
Called after a new object is saved. Use it for side effects like creating related objects or sending notifications.
def post_create(self) -> None:
PersonProfile(person=self).save()
async def apost_create(self) -> None:
await PersonProfile(person=self).asave()
pre_update / apre_update¶
Called before an existing object is updated. Use refetch_from_db() to compare with the current database state.
def pre_update(self) -> None:
original = self.refetch_from_db()
if original.name != self.name:
msg = 'Name cannot be changed'
raise ValueError(msg)
async def apre_update(self) -> None:
original = await self.arefetch_from_db()
if original.name != self.name:
msg = 'Name cannot be changed'
raise ValueError(msg)
post_update / apost_update¶
Called after an existing object is updated.
def post_update(self) -> None:
send_email(self.email, subject=f'{self.name}, your profile was updated')
async def apost_update(self) -> None:
send_email(self.email, subject=f'{self.name}, your profile was updated')
pre_delete / apre_delete¶
Called before an object is deleted. Raise an exception to prevent deletion.
def pre_delete(self) -> None:
if self.account_balance < 0:
msg = 'Cannot delete account with negative balance'
raise ValueError(msg)
async def apre_delete(self) -> None:
if self.account_balance < 0:
msg = 'Cannot delete account with negative balance'
raise ValueError(msg)
post_delete / apost_delete¶
Called after an object is deleted.
def post_delete(self) -> None:
send_email(self.email, subject=f'{self.name}, your profile was deleted')
async def apost_delete(self) -> None:
send_email(self.email, subject=f'{self.name}, your profile was deleted')
Note
Bulk operations (bulk_create, bulk_update, bulk_delete) do not trigger hooks.