Skip to content

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():

user = User(pk="USER#123", sk="PROFILE", name="John", age=30)
user.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:

user = User.get(pk="USER#123")

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:

user = User.get(pk="USER#123", sk="PROFILE")
user.name = "Jane"
user.age = 31
user.save()

Partial update with update(): Update specific fields without touching others:

user = User.get(pk="USER#123", sk="PROFILE")
user.update(name="Jane", age=31)

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:

user = User.get(pk="USER#123", sk="PROFILE")
user.delete()

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

data = {'pk': 'USER#123', 'sk': 'PROFILE', 'name': 'John'}
user = User.from_dict(data)

Skipping hooks

If you have lifecycle hooks but want to skip them for a specific operation:

user.save(skip_hooks=True)
user.delete(skip_hooks=True)
user.update(skip_hooks=True, name="Jane")

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:

class User(Model):
    model_config = ModelConfig(table="users", skip_hooks=True)

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