Usage Guide

This comprehensive guide covers all aspects of using sqlatypemodel in production applications.

Table of Contents

  1. Getting Started

  2. Pydantic Integration

  3. Python Dataclasses

  4. Attrs Integration

  5. Loading Strategies: Eager vs Lazy

  6. Async Support

  7. Custom Serialization

  8. Error Handling

  9. Advanced Patterns

Getting Started

The core concept is simple: replace standard Python types in your database models with sqlatypemodel-enhanced classes.

  1. Inherit: Make your data model inherit from MutableMixin (or LazyMutableMixin).

  2. Wrap: Use ModelType(YourModel) in the SQLAlchemy column definition.

  3. 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:

  1. Update version in pyproject.toml

  2. Update CHANGELOG.md

  3. Commit and push to master

  4. Create a GitHub Release (UI)

  5. ✅ Automated: Package published to PyPI in ~2-3 minutes!

See .github/WORKFLOWS.md for publishing workflow details.