Usage Guide¶
This comprehensive guide covers all aspects of using sqlatypemodel in production applications.
Table of Contents¶
Getting Started¶
The core concept is simple: replace standard Python types in your database models with sqlatypemodel-enhanced classes.
Inherit: Make your data model inherit from
MutableMixin(orLazyMutableMixin).Wrap: Use
ModelType(YourModel)in the SQLAlchemy column definition.Use: Mutate objects freely; changes are auto-saved.
Pydantic Integration¶
Pydantic is the primary citizen of sqlatypemodel. We support Pydantic V2 fully.
Basic Model¶
from pydantic import BaseModel
from sqlatypemodel import MutableMixin
class Profile(MutableMixin, BaseModel):
bio: str | None = None
preferences: dict[str, bool] = {}
Entity Definition¶
from sqlalchemy.orm import Mapped, mapped_column
from sqlatypemodel import ModelType
class User(Base):
__tablename__ = "users"
# ...
profile: Mapped[Profile] = mapped_column(ModelType(Profile))
Nested Models¶
You can nest Pydantic models arbitrarily. Changes deep in the hierarchy bubble up.
class Address(MutableMixin, BaseModel):
city: str
zip: str
class UserData(MutableMixin, BaseModel):
addresses: list[Address] = []
# Usage
user.data.addresses[0].city = "New York" # Detected!
Python Dataclasses¶
We provide first-class support for standard library dataclasses.
The Safe Wrapper¶
While standard dataclasses work, Python 3.12+ introduced optimizations that can cause recursion loops during initialization of mutable tracking objects. We recommend using our wrapper which enforces safe defaults (eq=False, slots=False).
from sqlatypemodel.util.dataclasses import dataclass
from sqlatypemodel import MutableMixin
@dataclass
class Config(MutableMixin):
host: str
port: int = 8080
Manual Mapping¶
For dataclasses, you usually provide a serializer/deserializer to ModelType, as they don’t have built-in .model_dump() methods like Pydantic.
from dataclasses import asdict
col = mapped_column(
ModelType(
Config,
dumper=asdict,
loader=lambda d: Config(**d)
)
)
Attrs Integration¶
Similar to dataclasses, we support attrs.
from attrs import asdict, define
from sqlatypemodel.util.attrs import define as safe_define
from sqlatypemodel import MutableMixin
@safe_define
class AppState(MutableMixin):
status: str
# Mapping
col = mapped_column(
ModelType(
AppState,
dumper=asdict,
loader=lambda d: AppState(**d)
)
)
Loading Strategies: Eager vs Lazy¶
Choosing the right mixin is the most important performance decision.
MutableMixin (Eager)¶
Behavior: Scans and wraps the entire object graph immediately upon loading from the database.
Pros: * Fastest attribute access after load (no “jit tax”). * Predictable performance profile.
Cons: * Slower DB load time for large objects. * Higher memory usage (wrappers created for everything).
Best For: Write-heavy scenarios, small objects, or when you traverse the whole object anyway.
LazyMutableMixin (Lazy)¶
Behavior: Loads raw data. Wraps only what you touch.
Pros: * 2x Faster DB loads. * 35% Less memory.
Cons: * First access to any field is slower (wrapping overhead).
Best For: Read-heavy scenarios, large documents where you only read a few fields (e.g. API filtering).
Async Support¶
sqlatypemodel is fully compatible with sqlalchemy.ext.asyncio.
Helpers¶
We provide helpers to create async engines with optimized JSON serializers pre-configured.
from sqlatypemodel.util.sqlalchemy import create_async_engine
engine = create_async_engine("postgresql+asyncpg://...")
Context Manager¶
When using AsyncSession, the workflow is identical to sync code.
async with AsyncSession(engine) as session:
user = await session.get(User, 1)
user.settings.theme = "dark"
await session.commit()
Custom Serialization¶
By default, ModelType attempts to use Pydantic’s model_dump(mode='json') for serialization and model_validate for deserialization. You can override this behavior.
Using dumper and loader¶
This is useful for non-Pydantic models (like Dataclasses) or when you need custom logic.
def my_dumper(obj: MyModel) -> dict:
return {"custom_field": obj.field}
def my_loader(data: dict) -> MyModel:
return MyModel(field=data["custom_field"])
col = mapped_column(
ModelType(
MyModel,
dumper=my_dumper,
loader=my_loader
)
)
Error Handling¶
The library provides specific exceptions for serialization failures.
Exceptions¶
SQLATypeModelError: Base exception for all library errors.SerializationError: Raised when an object cannot be converted to a dictionary for DB storage.DeserializationError: Raised when database data cannot be converted back to a model instance.
Usage¶
from sqlatypemodel.exceptions import DeserializationError
try:
session.commit()
except DeserializationError as e:
print(f"Failed to load model {e.model_name}: {e.original_error}")
Advanced Patterns¶
Batching Changes¶
If you are performing thousands of mutations in a loop, you can suppress intermediate signals to save CPU cycles.
with user.settings.batch_changes():
for _ in range(1000):
user.settings.log.append("entry")
# Single change notification fired here
Deep Nesting¶
The library handles arbitrary nesting (dicts of lists of models of dicts…). By default, there is a recursion limit of 100 to prevent stack overflows. You can customize this:
class DeepModel(MutableMixin, BaseModel):
_max_nesting_depth = 500
# ...
Pickling & Caching¶
You can pickle your models and store them in Redis/Memcached. When unpickled, they automatically reconnect their internal tracking mechanisms.
import pickle
# Save to cache
data = pickle.dumps(user.settings)
redis.set("user:1:settings", data)
# Load from cache
settings = pickle.loads(redis.get("user:1:settings"))
settings.theme = "blue" # Tracking still works!
Development & CI/CD¶
Code Quality¶
Before contributing, ensure all checks pass locally:
# Install pre-commit hooks
pre-commit install
# Run all quality checks
pre-commit run --all-files
# Or manually:
poetry run ruff check src/sqlatypemodel tests --fix
poetry run ruff format src/sqlatypemodel tests
poetry run mypy src/sqlatypemodel
Testing¶
Run tests locally to ensure your changes work:
# All tests
poetry run pytest -v
# With coverage
poetry run pytest -v --cov=sqlatypemodel --cov-report=term-missing
# Specific test category
poetry run pytest tests/unit/ -v
GitHub Actions¶
All code is automatically tested via GitHub Actions:
tests.yml: Tests on Python 3.10-3.14 with databases
lint.yml: Code quality checks (ruff, mypy, pre-commit)
security.yml: Weekly security scanning
docs.yml: Documentation building
publish.yml: Automated PyPI publishing on release
See .github/WORKFLOWS.md for complete information.
Releases¶
Releases are fully automated:
Update version in
pyproject.tomlUpdate
CHANGELOG.mdCommit and push to
masterCreate a GitHub Release (UI)
✅ Automated: Package published to PyPI in ~2-3 minutes!
See .github/WORKFLOWS.md for publishing workflow details.