ORM & Data Access · Python

SQLAlchemy & Django ORM

Python has two answers to the persistence question. SQLAlchemy is the toolkit — Core for SQL expressions, ORM on top, use either or both. Django ORM is the framework — pre-wired, batteries-included, inseparable from the Django request cycle. Picking one usually picks itself based on the framework.

PythonSQLAlchemy 2.0DjangoAlembic
← Back to Database Side
Quick Facts

At a Glance

Core Ideas

  • SQLAlchemy Core: SQL expression language — compose queries as Python objects. No ORM required.
  • SQLAlchemy ORM: declarative models, sessions, identity map, unit-of-work. Built on Core.
  • Django ORM: active-record style — Model.objects.filter(...). Tied to settings.py and Django's lifecycle.
  • QuerySet: Django's lazy chainable query — nothing executes until you iterate or slice.
  • Migrations: Alembic for SQLAlchemy; built-in makemigrations/migrate for Django.
The Pieces

What You'll Touch

SQLAlchemy 2.0

The modern API — typed, async-capable, unified across Core and ORM. The default for FastAPI, Flask, scripts, data pipelines.

Alembic

SQLAlchemy's migration tool. Autogenerates from model diffs; humans review and edit before merging.

Django ORM

Ships with Django. Integrated with the admin, forms, signals, and the auth system. You don't pick it; Django picks it for you.

Tortoise / Peewee

Async-first (Tortoise) and minimalist (Peewee) alternatives. Niche but well-loved.

SQLModel

SQLAlchemy + Pydantic in one. Popular with FastAPI for unifying database models and request schemas.

asyncpg / aiopg

The drivers underneath async SQLAlchemy on Postgres — raw-SQL speed when you don't need an ORM.

Picking One

Which Should You Use?

If You're in Django, Use Django ORM

The whole framework — admin, generic views, forms, auth — assumes Django models. Swapping in SQLAlchemy means losing all of that. The Django ORM is less expressive than SQLAlchemy, but the integration is worth more than the expressiveness for typical web apps.

If You're in FastAPI / Flask / Scripts, Use SQLAlchemy

It's the de-facto standard outside Django. Async support, typed mappings, and the Core layer for when you need raw SQL power without leaving Python. SQLModel is a friendly façade if you want less boilerplate.

If You're Doing Data Engineering, Lean Core

ETL, analytics, scripts that move millions of rows — the ORM's identity map and change tracking are pure overhead. Use SQLAlchemy Core to compose SQL safely, or drop to asyncpg/psycopg directly.

The Sharp Edges

Where People Get Cut

Django QuerySet Laziness

orders.filter(...).filter(...) doesn't execute until iterated. The trap is a queryset evaluated twice in the same view — two SQL trips. Cache with list(), or use prefetch_related / select_related to fold related lookups into one query.

N+1 in Both

Django: forget select_related on a foreign key, and every order.customer in a loop fires a query. SQLAlchemy: lazy-loaded relationships do the same. Both have eager-loading options (joinedload, selectinload); both punish you when you forget.

Migrations Get Out of Sync

Two branches each generate a migration with the same parent — when both merge, Alembic and Django both refuse to apply. Resolve with alembic merge or makemigrations --merge. Common enough on busy teams that it's worth knowing the muscle memory.

Connection Pooling in Async Code

Long-running async tasks holding sessions exhaust the pool. Use async with contexts so sessions close on exit, and size the pool deliberately — not all defaults match what async workers need.

SQLAlchemy 1.x vs 2.0

The 2.0 API changed how queries are written (select(User).where(...) instead of session.query(User).filter(...)). Tutorials and Stack Overflow answers mix both freely. New code should be 2.0-style; old code may need migrating.

Patterns

How Teams Actually Use Them

  • Django: custom Manager/QuerySet classes for reusable filters (.published(), .for_user(u)) — keeps views thin.
  • Django: F() expressions for atomic updates instead of read-modify-write.
  • SQLAlchemy: session-per-request via FastAPI/Flask dependencies; never module-level.
  • SQLAlchemy: reach for Core (insert().values(...) + execute) when bulk-loading; the ORM path is too slow for big batches.
  • Both: raw SQL is always available — raw(), extra(), text(). Use it when the ORM costs more than it saves.
Continue

Related