An ORM is useful infrastructure, but it is not a required layer in every Python backend. When the database is PostgreSQL, the schema already exists, and queries rely on database-specific behavior, I often prefer direct SQL with asyncpg.
async def fetch_session(
pool: asyncpg.Pool,
user_id: str,
session_id: UUID,
) -> dict[str, Any] | None:
sql = """
select id, name, status, started_at, completed_at
from workout_sessions
where id = $1 and user_id = $2::uuid
"""
async with pool.acquire() as connection:
row = await connection.fetchrow(sql, session_id, user_id)
return dict(row) if row else None
The selected columns, tenant boundary, casts, and query are visible together. Parameters remain separate from SQL text.
What direct SQL gives me#
- Explicit joins, projections, and ordering
- Natural access to
ON CONFLICT, CTEs, window functions, and row locks - No accidental lazy queries
- Clear control over transactions
- Fewer mappings when PostgreSQL is the intended database
It also makes database knowledge unavoidable. Developers must understand indexes, query plans, constraints, null semantics, and safe migrations.
Keep SQL behind repositories#
Direct SQL should not mean SQL in route functions. Repository functions own persistence details; services speak in application operations.
Map rows before returning them widely. Passing raw Record objects across the system couples domain code to asyncpg and database column names.
When I would add an ORM#
An ORM becomes attractive when the product is dominated by conventional related CRUD, the team benefits from model-driven migrations, or query composition is more important than database-specific control. Django's ORM is particularly valuable when used with Django admin and its broader model ecosystem.
SQLAlchemy can also provide transaction and mapping structure without requiring Django.
The decision is not “raw SQL is fast.” Poor direct SQL can be slow and unsafe. The decision is whether explicit database behavior or model-level productivity removes more risk for this application. Choose deliberately, then give that choice a clear boundary.