Type Checking
pydynox supports type checkers like mypy. This guide covers how to get the best type checking experience.
Supported Type Checkers
| Type Checker | Status | Notes |
|---|---|---|
| mypy | ✅ Tested | Recommended |
| pyright | ✅ Tested | Works well |
| ty (red-knot) | ⚠️ Partial | Descriptors not fully supported yet |
We test with mypy and pyright, but can't guarantee 100% coverage for all edge cases. If you find type issues, please open an issue.
How It Works
pydynox uses Python descriptors to make attributes work like regular values:
"""Basic type checking example - attribute types."""
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)
name = StringAttribute()
age = NumberAttribute()
active = BooleanAttribute()
user = User(pk="USER#1", name="John", age=30, active=True)
# On instance: returns the value (T | None)
pk_value: str | None = user.pk
name_value: str | None = user.name
age_value: float | None = user.age
active_value: bool | None = user.active
# On class: returns the Attribute (for conditions)
# User.pk -> StringAttribute
# User.pk == "USER#1" -> Condition
Attribute Types
Each attribute returns a specific type:
| Attribute | Returns |
|---|---|
StringAttribute |
str or None |
NumberAttribute |
float or None |
BooleanAttribute |
bool or None |
BinaryAttribute |
bytes or None |
ListAttribute |
list[Any] or None |
MapAttribute |
dict[str, Any] or None |
StringSetAttribute |
set[str] or None |
NumberSetAttribute |
set[int or float] or None |
JSONAttribute |
dict[str, Any] or list[Any] or None |
DatetimeAttribute |
datetime or None |
TTLAttribute |
datetime or None |
CompressedAttribute |
str or None |
EncryptedAttribute |
str or None |
S3Attribute |
S3Value or None |
VersionAttribute |
int or None |
CRUD Method Return Types
"""CRUD operations type checking example."""
from typing import TYPE_CHECKING, Any
from pydynox import Model, ModelConfig
from pydynox.attributes import StringAttribute
from pydynox.model import ModelQueryResult, ModelScanResult
# Type-only tests - wrapped in TYPE_CHECKING to avoid runtime execution
# These show what types mypy expects
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
sk = StringAttribute(sort_key=True)
name = StringAttribute()
if TYPE_CHECKING:
user = User(pk="USER#1", sk="PROFILE", name="John")
# get() returns M | dict[str, Any] | None
fetched: User | dict[str, Any] | None = User.get(pk="USER#1", sk="PROFILE")
# save(), delete(), update() return None
user.save()
user.delete()
user.update(name="Jane")
# query() returns ModelQueryResult[M]
query_result: ModelQueryResult[User] = User.query(partition_key="USER#1")
# Iterating - use isinstance to narrow the type
for item in User.query(partition_key="USER#1"):
if isinstance(item, User):
name: str | None = item.name
# scan() returns ModelScanResult[M]
scan_result: ModelScanResult[User] = User.scan()
# batch_get() returns list[M] | list[dict[str, Any]]
batch: list[User] | list[dict[str, Any]] = User.batch_get([{"pk": "USER#1", "sk": "PROFILE"}])
# from_dict() returns M
user_from_dict: User = User.from_dict({"pk": "USER#1", "sk": "PROFILE", "name": "Test"})
Common Issues
1. "None" in return types
All attributes can be None because:
- The attribute might not be set
- The attribute has
null=True(default)
If you know a value is not None, use assertion or narrowing:
"""How to handle None values in type checking."""
from pydynox import Model, ModelConfig
from pydynox.attributes import StringAttribute
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
name = StringAttribute()
# All attributes can be None, so you need to handle it
def get_name_with_default(user: User) -> str:
"""Return name or empty string if None."""
return user.name or ""
def get_name_with_check(user: User) -> str:
"""Raise if name is None."""
if user.name is None:
raise ValueError("User has no name")
return user.name
def get_name_safe(user: User) -> str | None:
"""Return name as-is (may be None)."""
return user.name
2. Union types from query/scan
query() and scan() return items that can be Model or dict because of the as_dict parameter. Use isinstance to narrow:
for item in User.query(partition_key="USER#1"):
if isinstance(item, User):
# mypy knows item is User
print(item.name)
Or if you know you're not using as_dict=True:
from typing import cast
for item in User.query(partition_key="USER#1"):
user = cast(User, item)
print(user.name)
3. ty (red-knot) doesn't understand descriptors
ty is a new type checker that doesn't fully support Python descriptors yet. If you see errors like:
Use mypy or pyright instead. ty will improve over time.
4. Pydantic models work out of the box
Pydantic models have native type support:
from pydantic import BaseModel
from pydynox.integrations.pydantic import dynamodb_model
@dynamodb_model(table="products", partition_key="pk")
class Product(BaseModel):
pk: str
name: str
price: float
product = Product(pk="PROD#1", name="Widget", price=9.99)
product.pk # str (not str | None)
product.name # str
product.price # float
Best Practices
1. Use mypy for type checking
2. Use TYPE_CHECKING for type-only imports
3. Handle None values explicitly
See the examples above for patterns to handle None values.
IDE Support
pydynox includes py.typed marker, so IDEs like VS Code and PyCharm will:
- Show correct types on hover
- Autocomplete attribute names
- Warn about type mismatches
Make sure your IDE is configured to use mypy or pyright for type checking.