Connection setup
SQLAlchemy v2 with thepsycopg (v3) driver is the most common pairing on modern projects. The Database URL becomes a SQLAlchemy URL by changing the prefix:
poolclass=NullPool— SQLAlchemy’s defaultQueuePoolkeeps connections alive across requests, but you already have PgBouncer doing that. Stacking two pools wastes connections and complicates debugging.NullPoolopens a connection per checkout and closes it on return.connect_args={"prepare_threshold": None}— disablespsycopgv3’s auto-prepare. Without this, you’ll get sporadicprepared statement "..." does not existerrors at runtime.
psycopg2 (the older C-based driver), the URL prefix is postgresql+psycopg2:// and the prepared-statement flag isn’t needed (psycopg2 doesn’t auto-prepare). Most new projects should use psycopg v3.
Declarative models
Mapped and mapped_column are the v2-style annotations. They give you fully-typed model attributes — user.email: str, not Column[str].
Sessions and queries
with Session(engine) as session: context manager handles connection lifecycle correctly for NullPool — the connection is opened on first query, returned on exit. Don’t reuse a session across HTTP requests.
For web apps, use the per-request session pattern your framework provides (Flask-SQLAlchemy’s db.session, FastAPI’s Depends(get_db) dependency, etc.). Each pattern wraps Session(engine) and ensures cleanup.
Alembic migrations
alembic.ini (after alembic init alembic):
alembic/env.py:
NullPool + prepare_threshold=None pattern as your app’s engine. Alembic’s autogenerate diffs your models against the live database and writes a migration:
alembic/versions/ is plain Python — review it before applying. Autogenerate is usually right but occasionally misses subtleties (column renames look like drop+add, custom check constraints aren’t picked up, etc.).
Alembic tracks state in alembic_version (a single-row table). Don’t touch it.
RLS from SQLAlchemy
The connection issupabase_admin, bypassing RLS. To run queries as a specific user:
SET LOCAL statements must be in the same transaction as the queries. The pattern works for read-mostly backend code; for per-request RLS, the cleaner separation is to use PostgREST (/rest/v1/*) under the user’s JWT for those reads and SQLAlchemy as supabase_admin for everything else.
SQLAlchemy in async
For async apps (FastAPI withasyncio, etc.), use the async engine and psycopg’s async support:
asyncpg (a different async driver), use statement_cache_size=0 instead of prepare_threshold=None:
Next steps
Connection pooling
Why
NullPool and prepare_threshold=None are required.Migrations
Alembic in the context of the other ORMs’ migration tools.
Direct Postgres
For SQL SQLAlchemy doesn’t express — bulk imports, schema introspection.
TypeORM
The other ORM Python-and-Node teams sometimes share.