Skip to main content
SQLAlchemy is the dominant Python ORM. Most Powabase teams writing Python use it. The migration runner that pairs with SQLAlchemy is Alembic, maintained by the same author. This guide covers both. For pooler-level constraints, see Connection pooling. For migration patterns shared across ORMs, see Migrations.

Connection setup

SQLAlchemy v2 with the psycopg (v3) driver is the most common pairing on modern projects. The Database URL becomes a SQLAlchemy URL by changing the prefix:
Two flags doing real work:
  • poolclass=NullPool turns off SQLAlchemy’s own pool. The default QueuePool keeps connections alive across requests, but PgBouncer already does that. Stacking two pools wastes connections and complicates debugging. NullPool opens a connection per checkout and closes it on return.
  • connect_args={"prepare_threshold": None} disables psycopg v3’s auto-prepare. Without it, you’ll get sporadic prepared statement "..." does not exist errors at runtime.
For 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

The 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):
In practice, you’ll read the URL from an env var rather than hardcoding. In alembic/env.py:
The same 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:
The generated migration file in 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 is supabase_admin, bypassing RLS. To run queries as a specific user:
The 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 with asyncio, and the like), use the async engine and psycopg’s async support:
For asyncpg (a different async driver), use statement_cache_size=0 instead of prepare_threshold=None:
Both flags do the same thing: disable prepared statements for pooler compatibility.

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.