Python
ORM
Database
Programming
Software Development

What are some good Python ORM solutions?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

There is no single best Python ORM for every project. The right choice depends on framework alignment, query complexity, migration tooling, async needs, and how much SQL control your team wants. A practical evaluation compares a few mature options on real workload queries instead of choosing by popularity.

SQLAlchemy ORM for Flexible Architecture

SQLAlchemy is widely used because it scales from simple models to complex query composition and explicit SQL control.

Minimal SQLAlchemy example:

python
1from sqlalchemy import String, create_engine, select
2from sqlalchemy.orm import DeclarativeBase, Mapped, Session, mapped_column
3
4
5class Base(DeclarativeBase):
6    pass
7
8
9class User(Base):
10    __tablename__ = "users"
11
12    id: Mapped[int] = mapped_column(primary_key=True)
13    email: Mapped[str] = mapped_column(String(255), unique=True)
14
15
16engine = create_engine("sqlite:///app.db", echo=False)
17Base.metadata.create_all(engine)
18
19with Session(engine) as session:
20    session.add(User(email="[email protected]"))
21    session.commit()
22
23with Session(engine) as session:
24    rows = session.scalars(select(User)).all()
25    print([u.email for u in rows])

Why teams choose it:

  • strong ecosystem and long-term maturity
  • explicit transaction control
  • good migration story with Alembic
  • supports advanced query patterns without hiding SQL

Django ORM for Django-First Products

If you are already using Django, its built-in ORM is productive and deeply integrated with admin, auth, and migration tooling.

python
1from django.db import models
2
3
4class Customer(models.Model):
5    email = models.EmailField(unique=True)
6    is_active = models.BooleanField(default=True)
7
8
9active_customers = Customer.objects.filter(is_active=True).order_by("email")
10for customer in active_customers[:10]:
11    print(customer.email)

Strengths:

  • fast development for web products
  • built-in migrations and admin tooling
  • consistent conventions for teams

Tradeoff is tighter coupling to Django project structure.

Peewee and Lightweight Options

For smaller services and scripts, Peewee can be a clean lightweight ORM with less setup overhead.

python
1from peewee import BooleanField, CharField, Model, SqliteDatabase
2
3db = SqliteDatabase("app.db")
4
5
6class BaseModel(Model):
7    class Meta:
8        database = db
9
10
11class Customer(BaseModel):
12    email = CharField(unique=True)
13    is_active = BooleanField(default=True)
14
15
16db.connect()
17db.create_tables([Customer])
18Customer.create(email="[email protected]", is_active=True)
19for c in Customer.select().where(Customer.is_active == True):
20    print(c.email)

This is useful for smaller scope projects where full framework integration is unnecessary.

Async and Modern Typed Workflows

If your service is async-heavy, evaluate ORM layers that support async sessions clearly. SQLAlchemy has async support, and some teams pair ORM models with query builders depending on workload.

Async SQLAlchemy example:

python
1import asyncio
2from sqlalchemy import String, select
3from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
4from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
5
6
7class Base(DeclarativeBase):
8    pass
9
10
11class Event(Base):
12    __tablename__ = "events"
13    id: Mapped[int] = mapped_column(primary_key=True)
14    name: Mapped[str] = mapped_column(String(100))
15
16
17async def main() -> None:
18    engine = create_async_engine("sqlite+aiosqlite:///events.db")
19    async with engine.begin() as conn:
20        await conn.run_sync(Base.metadata.create_all)
21
22    async with AsyncSession(engine) as session:
23        session.add(Event(name="startup"))
24        await session.commit()
25
26    async with AsyncSession(engine) as session:
27        rows = (await session.execute(select(Event))).scalars().all()
28        print([r.name for r in rows])
29
30    await engine.dispose()
31
32
33asyncio.run(main())

Choose async ORM features only if your stack genuinely benefits from async database IO.

Evaluation Criteria That Matter

Use criteria tied to your workload:

  1. query expressiveness for complex reports
  2. migration reliability in production
  3. transaction semantics and unit-of-work behavior
  4. performance transparency with generated SQL inspection
  5. typing and tooling support for your team

A short proof-of-concept with production-like queries is far more valuable than feature checklists.

One practical test is to implement the same moderately complex query in each candidate ORM, then inspect the generated SQL and migration story. That exercise reveals far more than a feature matrix because it exposes how the tool feels under the exact workload your team expects to maintain.

Avoid ORM-Only Thinking

ORMs reduce boilerplate but do not replace SQL understanding. For critical paths, inspect generated SQL and indexes.

With SQLAlchemy, for example, you can inspect emitted SQL via logging and verify query plans in your database.

python
engine = create_engine("postgresql+psycopg://app:pass@localhost/app", echo=True)

This helps catch N+1 queries and missing indexes early.

Common Pitfalls

A common pitfall is choosing an ORM based only on popularity and ignoring migration or operational needs. Another is assuming ORM abstraction removes the need to understand SQL plans and indexes. Teams also mix multiple ORM styles in one service without clear boundaries, which increases maintenance cost. Async ORM adoption without async infrastructure alignment is another frequent mismatch. Finally, skipping proof-of-concept benchmarking leads to late performance surprises.

Summary

  • Pick a Python ORM based on workload, framework context, and operational requirements.
  • SQLAlchemy is a strong general-purpose choice with flexible query control.
  • Django ORM is excellent for Django-centric applications with fast product development goals.
  • Lightweight options can be effective for smaller projects and scripts.
  • Validate any ORM choice with real queries, migrations, and performance checks.

Course illustration
Course illustration

All Rights Reserved.