Skip to content

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.