From python-foundation
Framework-agnostic SQLAlchemy 2.0 core shared by fastapi-plugin and flask-plugin: declarative mapped classes with Mapped/mapped_column, column type selection, relationships with explicit lazy loading and cascades, 2.0-style select() querying, transaction/flush discipline, and Alembic-agnostic migration metadata rules. Framework plugins layer their delta skills (async sessions for FastAPI, Flask-SQLAlchemy integration for Flask) on top of this skill. Use this skill to: - Write SQLAlchemy 2.0 declarative models with Mapped[T] annotations and mapped_column(). - Pick correct column types (String(N), Numeric, DateTime(timezone=True), Uuid, Enum). - Define relationships with explicit lazy loading strategy and cascade settings. - Query with 2.0-style select() statements and manage flush vs commit boundaries. - Keep model metadata visible to Alembic autogenerate. Do NOT use this skill for: - FastAPI async engine/session lifecycle — see fastapi-plugin:sqlalchemy-patterns. - Flask-SQLAlchemy extension setup and Flask-Migrate — see flask-plugin:sqlalchemy-patterns. - Python idioms — see python-foundation:python-conventions.
How this skill is triggered — by the user, by Claude, or both
Slash command
/python-foundation:sqlalchemy-patternsThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Read `pyproject.toml` or `requirements.txt` before writing any model code:
Read pyproject.toml or requirements.txt before writing any model code:
grep -E "sqlalchemy" pyproject.toml requirements.txt
Mapped/mapped_column syntax (shown throughout this skill). This is the assumed baseline.Column()/relationship() style. Mark a comment in the code noting the legacy version; do not silently mix styles.Always prefer 2.0 style for new code. When working in an existing project using 1.x style throughout, match the existing style. Never mix 1.x Column() and 2.0 mapped_column() in the same model.
Define one Base class per project. All models inherit from it. (Framework plugins may supply the base for you — e.g., Flask-SQLAlchemy's db.Model — but the mapped-column rules below are identical.)
from datetime import datetime
from decimal import Decimal
from typing import Optional
from sqlalchemy import DateTime, ForeignKey, Numeric, String, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column, relationship
class Base(DeclarativeBase):
pass
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True)
display_name: Mapped[str] = mapped_column(String(100))
hashed_password: Mapped[str] = mapped_column(String(255))
is_active: Mapped[bool] = mapped_column(default=True)
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now()
)
orders: Mapped[list["Order"]] = relationship(
"Order", back_populates="user", lazy="selectin"
)
def __repr__(self) -> str:
return f"<User id={self.id} email={self.email!r}>"
Key rules:
Mapped[T] without Optional means NOT NULL. Mapped[Optional[T]] means nullable.mapped_column() without a SQLAlchemy type uses Python type inference — always provide the type explicitly (e.g., String(255)) so the migration specialist can finalize column lengths, precision, and constraints correctly.server_default=func.now() for database-side default timestamps, not Python-side default=datetime.utcnow (Python-side defaults are not reflected in the DB schema, and utcnow is deprecated).| Python type | SQLAlchemy column type | Notes |
|---|---|---|
str | String(N) | Always set length; never bare String |
Decimal | Numeric(precision, scale) | Never Float for money or precise values |
datetime | DateTime(timezone=True) | Always set timezone=True |
int | Integer or BigInteger | Use BigInteger for large tables (users, events) |
bool | Boolean | |
UUID | Uuid (SA 2.0+) or String(36) | Uuid stores as native UUID on PostgreSQL |
| enum | Enum(MyEnum, native_enum=False) | native_enum=False for DB portability |
float | Float | Only for non-monetary approximations (lat/lon, scores) |
Always use 2.0-style select() statements executed through the session. The session/execution style (sync db.session vs async AsyncSession) comes from the framework delta skill; the statement construction is identical.
from sqlalchemy import select
from sqlalchemy.orm import selectinload
select(User).where(User.id == user_id) # single row by predicate
select(User).where(User.email == email) # lookup by unique column
select(User).offset(skip).limit(limit) # pagination
select(User).options(selectinload(User.orders)).where(...) # explicit eager load
Result handling rules:
scalar_one_or_none() for single-row queries.scalars().all() for multi-row queries.session.get(Model, pk) for primary-key lookups where the session API supports it.Transaction/flush discipline:
flush() inside a unit of work to get the generated PK without committing.get_db() dependency on yield exit; Flask: the view/service layer at the end of the request) — never scattered through helper functions.Define relationships with explicit lazy and cascade settings. Never rely on defaults.
from sqlalchemy.orm import Mapped, mapped_column, relationship
class User(Base):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True)
orders: Mapped[list["Order"]] = relationship(
"Order",
back_populates="user",
lazy="selectin",
cascade="all, delete-orphan",
)
profile: Mapped[Optional["UserProfile"]] = relationship(
"UserProfile",
back_populates="user",
lazy="selectin",
uselist=False,
cascade="all, delete-orphan",
)
class Order(Base):
__tablename__ = "orders"
id: Mapped[int] = mapped_column(primary_key=True)
user_id: Mapped[int] = mapped_column(ForeignKey("users.id", ondelete="CASCADE"))
user: Mapped["User"] = relationship("User", back_populates="orders", lazy="selectin")
lines: Mapped[list["OrderLine"]] = relationship(
"OrderLine",
back_populates="order",
lazy="selectin",
cascade="all, delete-orphan",
)
Structural rules:
back_populates.uselist=False for the scalar side of one-to-one relations.ondelete on the ForeignKey (e.g., "CASCADE") so the constraint matches the ORM cascade.Lazy loading strategy guide:
lazy="selectin" — loads the related collection with a separate SELECT IN query. Best for small-to-medium collections that are always needed. Safe in both sync and async contexts.lazy="select" — separate SELECT on first attribute access (SQLAlchemy default). Only safe in synchronous contexts; in async it triggers implicit IO and fails.lazy="joined" — loads the relation with a JOIN in the same query. Best for one-to-one relations or small, always-needed collections.lazy="raise" — raises if accessed without explicit eager loading, forcing selectinload()/joinedload() at query time. Best for large or rarely-needed collections to prevent N+1.lazy="subquery" — sync-only; not supported by async drivers.lazy="dynamic" — deprecated in SQLAlchemy 2.0. Do not use. Replace with explicit select() queries.Which strategies are permitted per framework is refined by the framework delta skill (async contexts forbid select/subquery).
To prevent N+1 queries on list endpoints, apply eager-load options at query time:
from sqlalchemy.orm import joinedload, selectinload
select(User).options(selectinload(User.orders))
Both FastAPI (raw Alembic) and Flask (Flask-Migrate, which wraps Alembic) rely on --autogenerate reading the metadata object (Base.metadata / db.metadata).
target_metadata is set. Any model module not imported is invisible to autogenerate and its tables are silently missed.app/db/base.py or app/models/__init__.py) so a single import guarantees complete metadata.--autogenerate; explicit column types (lengths, precision, timezone=True) are what make the generated migrations correct.Framework-specific wiring (async env.py for FastAPI, flask db init scaffolding for Flask) lives in the respective delta skill.
| Anti-pattern | Problem | Correct approach |
|---|---|---|
String without length | Alembic autogenerate produces VARCHAR with no length; some databases use TEXT or reject it | Always String(N) |
Float for monetary values | IEEE 754 rounding errors on financial calculations | Numeric(precision, scale) |
lazy="dynamic" | Deprecated in SQLAlchemy 2.0; raises a warning | Use lazy="selectin"/lazy="select" (per framework) or explicit selectinload() |
default=datetime.utcnow | Python-side default — not reflected in DB schema; utcnow is deprecated | server_default=func.now() with DateTime(timezone=True) |
| Importing models only in routers | Migration tooling never sees them; --autogenerate misses tables | Import all models in the central metadata module |
Mixing Column() and mapped_column() | Produces inconsistent metadata; confuses tooling | One style per project; prefer mapped_column() for new code |
Scattered commit() calls in helpers | Makes unit testing harder; unclear transaction boundaries | Commit once per unit of work at the framework-defined boundary; flush() for PKs |
npx claudepluginhub aratkruglik/claude-sdlc --plugin python-foundationProvides SQLAlchemy 2.0 patterns for Python database operations including ORM models, async engines, relationships, and common queries. Useful for robust DB layers in Python apps.
Provides SQLAlchemy 2.0 patterns for model definitions, sync/async engines, relationships, query optimization, repository pattern, and transactions in Python apps.
SQLAlchemy 2.0+ ORM patterns for Python database access: model definition, session management, queries, upserts, relationships, and JSON columns.