Lifecycle hooks
Run code before or after model operations. Hooks let you add validation, logging, or any custom logic without cluttering your main code.
Key features
- Validation before save
- Logging after operations
- Data transformation
- Side effects like sending emails
Getting started
Hooks are methods decorated with special decorators. When you call save(), delete(), or update(), pydynox automatically runs the matching hooks.
Available hooks
| Hook | When it runs |
|---|---|
@before_save |
Before save() |
@after_save |
After save() |
@before_delete |
Before delete() |
@after_delete |
After delete() |
@before_update |
Before update() |
@after_update |
After update() |
@after_load |
After get() or query |
Basic usage
Here's a common pattern: validate and normalize data before saving, then log after:
from pydynox import Model, ModelConfig
from pydynox.attributes import StringAttribute
from pydynox.hooks import after_save, before_save
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(hash_key=True)
email = StringAttribute()
name = StringAttribute()
@before_save
def validate_email(self):
if not self.email or "@" not in self.email:
raise ValueError("Invalid email")
@before_save
def normalize(self):
self.email = self.email.lower().strip()
self.name = self.name.strip()
@after_save
def log_save(self):
print(f"Saved user: {self.pk}")
# Hooks run automatically
user = User(pk="USER#1", email="JOHN@TEST.COM", name="john doe")
user.save() # Validates, normalizes, then logs
# Skip hooks if needed
user.save(skip_hooks=True)
In this example:
validate_emailruns first and raises an error if the email is invalidnormalizeruns next and cleans up the data- The item is saved to DynamoDB
log_saveruns last and prints a message
If any before_* hook raises an exception, the operation stops and the item is not saved.
Advanced
Multiple hooks of the same type
You can have multiple hooks of the same type. They run in the order they're defined in the class:
class User(Model):
@before_save
def first_hook(self):
print("This runs first")
@before_save
def second_hook(self):
print("This runs second")
All hooks example
Here's a model with all available hooks:
from pydynox import Model, ModelConfig
from pydynox.attributes import StringAttribute
from pydynox.hooks import (
after_delete,
after_load,
after_save,
after_update,
before_delete,
before_save,
before_update,
)
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(hash_key=True)
name = StringAttribute()
@before_save
def on_before_save(self):
print("Before save")
@after_save
def on_after_save(self):
print("After save")
@before_delete
def on_before_delete(self):
print("Before delete")
@after_delete
def on_after_delete(self):
print("After delete")
@before_update
def on_before_update(self):
print("Before update")
@after_update
def on_after_update(self):
print("After update")
@after_load
def on_after_load(self):
print("After load (get or query)")
Skipping hooks
Sometimes you need to bypass hooks. For example, during data migration or when fixing bad data.
Skip hooks for a single operation:
Or disable hooks for all operations on a model:
Warning
Be careful when skipping hooks. If you have validation in before_save, skipping it means invalid data can be saved.
Common patterns
| Pattern | Hook | Example |
|---|---|---|
| Validation | @before_save |
Check email format, required fields |
| Normalization | @before_save |
Lowercase email, trim whitespace |
| Timestamps | @before_save |
Set updated_at field |
| Logging | @after_save |
Log saved item ID |
| Audit | @after_save |
Write to audit table |
| Cleanup | @after_delete |
Delete related data, files |
| Transformation | @after_load |
Format dates, compute fields |
Hooks and transactions
Hooks run for each item in a transaction. If you're saving 10 items in a transaction, before_save runs 10 times.
If a hook raises an exception, the entire transaction fails and nothing is saved.