Skip to content

Getting started

This guide walks you through installing pydynox and creating your first model. By the end, you'll have a working DynamoDB model with CRUD operations.

Key features

  • Install with pip or uv
  • Define models with typed attributes
  • CRUD operations with simple methods
  • Local development with DynamoDB Local

Installation

pip install pydynox
uv add pydynox

To verify the installation:

import pydynox
print(pydynox.__version__)
# 0.1.0

# For detailed version info
print(pydynox.version_info())
#        pydynox version: 0.1.0
#        python version: 3.11.0
#               platform: macOS-14.0-arm64
#       related packages: boto3-1.34.0 pydantic-2.5.0

Your first model

Let's create a simple User model. A model is a Python class that represents items in a DynamoDB table.

from pydynox import Model, ModelConfig
from pydynox.attributes import BooleanAttribute, NumberAttribute, StringAttribute


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

    pk = StringAttribute(partition_key=True)
    sk = StringAttribute(sort_key=True)
    name = StringAttribute()
    age = NumberAttribute(default=0)
    active = BooleanAttribute(default=True)

Here's what each part does:

  • model_config = ModelConfig(table="users") - Configuration for the model. table is the DynamoDB table name.
  • pk = StringAttribute(partition_key=True) - The partition key. Every item needs one.
  • sk = StringAttribute(sort_key=True) - The sort key. Optional, but useful for complex access patterns.
  • Other attributes - Regular fields with their types and optional defaults.

Async-first

Python async is growing. FastAPI, aiohttp, Starlette - modern web frameworks are async. pydynox follows this trend.

Every async method has a sync version with sync_ prefix:

Async (default) Sync
await user.save() user.sync_save()
await User.get() User.sync_get()
await user.delete() user.sync_delete()
async for x in User.query() for x in User.sync_query()

Both versions release the GIL during network calls. The difference: async lets you run multiple operations at the same time with asyncio.gather(). Sync runs one after another.

For the full story on why async matters and how pydynox handles it, see Async-first.

Basic operations

Now let's use the model to work with DynamoDB. pydynox uses an async-first API - methods without prefix are async (default), methods with sync_ prefix are sync.

"""Example: CRUD operations (async - default)."""

import asyncio

from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute


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

    pk = StringAttribute(partition_key=True)
    sk = StringAttribute(sort_key=True)
    name = StringAttribute()
    age = NumberAttribute(default=0)


async def main():
    # Create
    user = User(pk="USER#123", sk="PROFILE", name="John", age=30)
    await user.save()

    # Read
    user = await User.get(pk="USER#123", sk="PROFILE")
    if user:
        print(user.name)  # John

    # Update - full
    user.name = "Jane"
    await user.save()

    # Update - partial
    await user.update(name="Jane", age=31)

    # Delete
    await user.delete()


if __name__ == "__main__":
    asyncio.run(main())
"""Example: CRUD operations (sync - use sync_ prefix)."""

from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute


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

    pk = StringAttribute(partition_key=True)
    sk = StringAttribute(sort_key=True)
    name = StringAttribute()
    age = NumberAttribute(default=0)


# Create
user = User(pk="USER#123", sk="PROFILE", name="John", age=30)
user.sync_save()

# Read
user = User.sync_get(pk="USER#123", sk="PROFILE")
if user:
    print(user.name)  # John

# Update - full
user.name = "Jane"
user.sync_save()

# Update - partial
user.sync_update(name="Jane", age=31)

# Delete
user.sync_delete()

Create

Instantiate your model and call save():

user = User(pk="USER#123", sk="PROFILE", name="John", age=30)
await user.save()  # async
user.sync_save()   # sync

Read

Use get() with the key attributes:

# Async
user = await User.get(pk="USER#123", sk="PROFILE")
if user:
    print(user.name)

# Sync
user = User.sync_get(pk="USER#123", sk="PROFILE")

Update

Change attributes and save, or use update() for partial updates:

# Full update (async)
user.name = "Jane"
await user.save()

# Partial update (async)
await user.update(name="Jane", age=31)

# Sync versions
user.sync_save()
user.sync_update(name="Jane", age=31)

Delete

Call delete() on an instance:

await user.delete()  # async
user.sync_delete()   # sync

Configuration

ModelConfig configures how your model connects to DynamoDB:

from pydynox import Model, ModelConfig

class User(Model):
    model_config = ModelConfig(
        table="users",              # Required - table name
        region="us-east-1",         # Optional - AWS region
        endpoint_url=None,          # Optional - for local testing
    )

Local development

pydynox has a built-in memory backend. No Docker, no setup - just a pytest fixture:

import pytest
from pydynox.testing import memory_backend

@pytest.fixture
def dynamo():
    with memory_backend():
        yield

def test_create_user(dynamo):
    user = User(pk="USER#123", name="John")
    user.sync_save()

    found = User.sync_get(pk="USER#123")
    assert found.name == "John"

The memory backend is fast and isolated. Each test starts with a clean slate.

For more details, see Testing.

DynamoDB Local

For integration tests that need real DynamoDB behavior, use DynamoDB Local:

docker run -p 8000:8000 amazon/dynamodb-local

Then point your model to it:

from pydynox import Model, ModelConfig

class User(Model):
    model_config = ModelConfig(
        table="users",
        endpoint_url="http://localhost:8000",
    )

Next steps

Now that you have the basics working: