A typed, async DynamoDB ORM for Python, with a Rust core. Declare models with type hints (or pydantic), and get validated, atomic, observable DynamoDB access: repositories, queries with cursors, transactions with readable failures, unique constraints, optimistic locking, single-table design and an in-memory DynamoDB for tests.
import asyncio
from datetime import datetime
import dynamoxy
from dynamoxy import Attr, field, partition_key
class User(dynamoxy.Model, table="users"):
user_id: Attr[str] = partition_key()
email: Attr[str] = field(unique=True)
name: Attr[str] = field(min_length=1)
visits: Attr[int] = field(default=0)
created_at: Attr[datetime] = dynamoxy.created_at()
version: Attr[int] = dynamoxy.version()
by_email = dynamoxy.GlobalIndex(partition_key=email)
async def main() -> None:
async with dynamoxy.Client(region="eu-west-1") as client: # or dynamoxy.testing.memory_client()
await client.ensure_tables(User)
users = client.repository(User)
ada = await users.create(User(user_id="u1", email="ada@example.com", name="Ada"))
await users.update(ada, User.visits.add(1)) # atomic; `ada` is refreshed in place
try:
await users.create(User(user_id="u2", email="ada@example.com", name="Imposter"))
except dynamoxy.UniqueConstraintError as error:
print(f"{error.field} is taken")
found = await users.query(index=User.by_email, email="ada@example.com").first()
print(found)
asyncio.run(main())Inherit from dynamoxy.PydanticModel instead of BaseModel and mark the key. Pydantic keeps
validating, and the class stays a pydantic model, so the same Customer can be a FastAPI request or
response model and a DynamoDB table (uv add 'dynamoxy[pydantic]'):
from typing import Annotated
from pydantic import BaseModel, Field
import dynamoxy
class Address(BaseModel): # plain pydantic models nest as maps
street: str
city: str
class Customer(dynamoxy.PydanticModel, table="customers"): # instead of BaseModel
customer_id: Annotated[str, dynamoxy.PartitionKey()]
email: Annotated[str, dynamoxy.Unique()]
name: str = Field(min_length=1)
address: Address | None = None
await client.ensure_tables(Customer)
customers = client.repository(Customer)
await customers.create(Customer(customer_id="c1", email="ada@example.com", name="Ada"))
customer = await customers.get("c1") # a validated CustomerA model you would rather not touch can stay as it is: subclass it together with
dynamoxy.PydanticModel and re-declare only the fields that need a marker, such as the key (see
the pydantic guide).
- Typed models:
Attr[T]fields are values on instances and typed expression paths on the class (User.visits > 3); type checkers understand constructors and expressions. Pydantic models work too. - Correct by default: validation before I/O,
createthat never overwrites, optimistic locking withversion(), unique attributes enforced transactionally, automatic timestamps, conditions on every write, no values in expression strings. - Every DynamoDB feature: queries and scans with cursors, global and local indexes, batches with unprocessed-item handling, transactions with per-operation cancellation reasons, TTL, streams, backups and point-in-time recovery, PartiQL, table management.
- Single-table design with key templates, derived index keys and mixed-entity collections.
- Observable: structured events and logging, hooks, middleware, OpenTelemetry spans and
metrics, per-request cost accounting (
dynamoxy.collect()). - Testable:
dynamoxy.testing.memory_client()is an in-memory DynamoDB with DynamoDB's semantics and fault injection; no Docker, no AWS account. - Fast and robust: a Rust core (AWS SDK for Rust, Tokio) handles transport, retries with backoff, timeouts and batching; wheels for Linux (x86_64, aarch64, glibc and musl), macOS and Windows, CPython 3.12+.
uv add dynamoxy # or: pip install dynamoxy
uv add 'dynamoxy[pydantic]' # pydantic models
uv add 'dynamoxy[otel]' # OpenTelemetry| Getting started | First model, connecting, first queries |
| Usage guide | The compact reference (dynamoxy guide) |
| Guides | Modeling, reading, writing, transactions, single-table design, testing, ... |
| Examples | Runnable programs |
| Reference | Configuration, errors, events |
| Architecture | How it works, invariants, decisions |
| Changelog | Release notes |
AI agents: run dynamoxy guide (or read AGENT_GUIDE.md)
before writing code with dynamoxy; application repositories can copy the skill in
skills/dynamoxy. Agents changing dynamoxy itself start with
AGENTS.md.
The Rust core (crates/dynamoxy-core) speaks DynamoDB through the AWS SDK for Rust, with its own
retries, timeouts, batching, events and metrics, and contains a complete in-memory DynamoDB. A
thin PyO3 binding exposes it to asyncio, and the Python package adds models, validation,
repositories, transactions, constraints, hooks and OpenTelemetry.
See CONTRIBUTING.md. The project is young (0.x): breaking changes are possible in minor versions and are called out in the changelog.