Models
Models define the structure of your DynamoDB items and provide CRUD operations.
Key features
- Typed attributes with defaults
- Hash key and range key support
- Required fields with
null=False - Save, get, update, delete operations
- Convert to/from dict
Getting started
Basic model
from pydynox import Model, ModelConfig
from pydynox.attributes import BooleanAttribute, NumberAttribute, StringAttribute
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(hash_key=True)
sk = StringAttribute(range_key=True)
name = StringAttribute()
age = NumberAttribute(default=0)
active = BooleanAttribute(default=True)
Tip
Want to see all supported attribute types? Check out the Attribute types guide.
Keys
Every model needs at least a hash key (partition key):
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(hash_key=True) # Required
Add a range key (sort key) for composite keys:
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(hash_key=True)
sk = StringAttribute(range_key=True) # Optional
Defaults and required fields
from pydynox import Model, ModelConfig
from pydynox.attributes import (
BooleanAttribute,
ListAttribute,
MapAttribute,
NumberAttribute,
StringAttribute,
)
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(hash_key=True)
email = StringAttribute(null=False) # Required field
name = StringAttribute(default="")
age = NumberAttribute(default=0)
active = BooleanAttribute(default=True)
tags = ListAttribute(default=[])
settings = MapAttribute(default={})
CRUD operations
Here's a complete example showing all CRUD operations:
from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(hash_key=True)
sk = StringAttribute(range_key=True)
name = StringAttribute()
age = NumberAttribute(default=0)
# Create
user = User(pk="USER#123", sk="PROFILE", name="John", age=30)
user.save()
# Read
user = User.get(pk="USER#123", sk="PROFILE")
if user:
print(user.name) # John
# Update - full
user.name = "Jane"
user.save()
# Update - partial
user.update(name="Jane", age=31)
# Delete
user.delete()
Create
To create a new item, instantiate your model and call save():
If an item with the same key already exists, save() replaces it completely. This is how DynamoDB works - there's no separate "create" vs "update" at the API level.
Read
To get an item by its key, use the class method get():
user = User.get(pk="USER#123", sk="PROFILE")
if user:
print(user.name)
else:
print("User not found")
get() returns None if the item doesn't exist. Always check for None before using the result.
If your table has only a hash key (no range key), you only need to pass the hash key:
Consistent reads
By default, get() uses eventually consistent reads. For strongly consistent reads, use consistent_read=True:
"""Consistent read examples."""
from pydynox import DynamoDBClient, Model, ModelConfig, StringAttribute
client = DynamoDBClient(region="us-east-1")
# Option 1: Per-operation (highest priority)
class User(Model):
model_config = ModelConfig(table="users", client=client)
pk = StringAttribute(hash_key=True)
sk = StringAttribute(range_key=True)
name = StringAttribute()
# Eventually consistent (default)
user = User.get(pk="USER#123", sk="PROFILE")
# Strongly consistent
user = User.get(pk="USER#123", sk="PROFILE", consistent_read=True)
# Option 2: Model-level default
class Order(Model):
model_config = ModelConfig(
table="orders",
client=client,
consistent_read=True, # All reads are strongly consistent
)
pk = StringAttribute(hash_key=True)
sk = StringAttribute(range_key=True)
# Uses strongly consistent read (from model_config)
order = Order.get(pk="ORDER#456", sk="ITEM#1")
# Override to eventually consistent for this call
order = Order.get(pk="ORDER#456", sk="ITEM#1", consistent_read=False)
When to use strongly consistent reads:
- You need to read data right after writing it
- Your app can't tolerate stale data (even for a second)
- You're building a financial or inventory system
Trade-offs:
| Eventually consistent | Strongly consistent | |
|---|---|---|
| Latency | Lower | Higher |
| Cost | 0.5 RCU per 4KB | 1 RCU per 4KB |
| Availability | Higher | Lower during outages |
Most apps work fine with eventually consistent reads. Use strongly consistent only when you need it.
Update
There are two ways to update an item:
Full update with save(): Change attributes and call save(). This replaces the entire item:
Partial update with update(): Update specific fields without touching others:
The difference matters when you have many attributes. With save(), you send all attributes to DynamoDB. With update(), you only send the changed ones.
update() also updates the local object, so user.name is "Jane" after the call.
Delete
To delete an item, call delete() on an instance:
After deletion, the object still exists in Python, but the item is gone from DynamoDB.
Advanced
ModelConfig options
| Option | Type | Default | Description |
|---|---|---|---|
table |
str | Required | DynamoDB table name |
client |
DynamoDBClient | None | Client to use (uses default if None) |
skip_hooks |
bool | False | Skip lifecycle hooks |
max_size |
int | None | Max item size in bytes |
consistent_read |
bool | False | Use strongly consistent reads by default |
Setting a default client
Instead of passing a client to each model, set a default client once:
from pydynox import DynamoDBClient, set_default_client
# At app startup
client = DynamoDBClient(region="us-east-1", profile="prod")
set_default_client(client)
# All models use this client
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(hash_key=True)
class Order(Model):
model_config = ModelConfig(table="orders")
pk = StringAttribute(hash_key=True)
Override client per model
Use a different client for specific models:
# Default client for most models
set_default_client(prod_client)
# Special client for audit logs
audit_client = DynamoDBClient(region="eu-west-1")
class AuditLog(Model):
model_config = ModelConfig(
table="audit_logs",
client=audit_client, # Uses different client
)
pk = StringAttribute(hash_key=True)
Converting to dict
user = User(pk="USER#123", sk="PROFILE", name="John")
data = user.to_dict()
# {'pk': 'USER#123', 'sk': 'PROFILE', 'name': 'John'}
Creating from dict
Skipping hooks
If you have lifecycle hooks but want to skip them for a specific operation:
This is useful for:
- Data migrations where validation might fail on old data
- Bulk operations where you want maximum speed
- Fixing bad data that wouldn't pass validation
You can also 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 to DynamoDB.
Error handling
DynamoDB operations can fail for various reasons. Common errors:
| Error | Cause |
|---|---|
TableNotFoundError |
Table doesn't exist |
ThrottlingError |
Exceeded capacity |
ValidationError |
Invalid data (item too large, etc.) |
ConditionCheckFailedError |
Conditional write failed |
Wrap operations in try/except if you need to handle errors:
from pydynox.exceptions import TableNotFoundError, ThrottlingError
try:
user.save()
except TableNotFoundError:
print("Table doesn't exist")
except ThrottlingError:
print("Rate limited, try again")
Next steps
- Attribute types - All available attribute types
- Indexes - Query by non-key attributes with GSIs
- Conditions - Conditional writes
- Hooks - Lifecycle hooks for validation