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=NullPoolturns off SQLAlchemy’s own pool. The defaultQueuePoolkeeps connections alive across requests, but PgBouncer already does 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 it, 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, since psycopg2 doesn’t auto-prepare. New projects should prefer 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 the first query and 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, and so on). Each 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 file:
alembic/versions/ is plain Python, so 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, and so on.
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. This works for read-mostly backend code. For per-request RLS, the cleaner split 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, and the like), 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.