Home Python SQLModel: Python ORM for FastAPI and SQLAlchemy
Intermediate 3 min · July 14, 2026

SQLModel: Python ORM for FastAPI and SQLAlchemy

Learn SQLModel, the modern Python ORM that combines SQLAlchemy and Pydantic for FastAPI.

N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
Before you start⏱ 15-20 min read
  • Python 3.7+ installed
  • Basic knowledge of SQL and databases
  • Familiarity with FastAPI or Flask
  • Understanding of Python type hints
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
Quick Answer

SQLModel is a Python ORM that merges SQLAlchemy's database power with Pydantic's data validation, designed for FastAPI. It lets you define models once, get automatic API schemas, and use async/await. Key benefits: less boilerplate, type safety, and seamless FastAPI integration.

✦ Definition~90s read
What is SQLModel?

SQLModel is a Python ORM that combines SQLAlchemy and Pydantic, allowing you to define database models and API schemas in a single class.

Think of SQLModel as a smart translator between your Python code and a database.
Plain-English First

Think of SQLModel as a smart translator between your Python code and a database. Normally, you'd write two sets of instructions: one for the database (SQLAlchemy) and one for data validation (Pydantic). SQLModel combines them into one, so you only write once. It's like having a bilingual friend who can talk to both the database and your API without repeating yourself.

When building APIs with FastAPI, you need to interact with a database. Traditionally, you'd use SQLAlchemy for ORM and Pydantic for data validation, leading to duplicated model definitions. SQLModel, created by FastAPI's author, solves this by merging both into a single model. This tutorial will guide you through SQLModel's core features, from basic CRUD to advanced relationships, with production-ready examples. You'll learn how to define models, perform queries, handle migrations, and debug issues in production. By the end, you'll be able to build robust FastAPI applications with minimal boilerplate.

Setting Up SQLModel

First, install SQLModel: pip install sqlmodel. It pulls in SQLAlchemy and Pydantic automatically. Create a database engine and session. Here's a minimal setup:

setup.pyPYTHON
1
2
3
4
5
6
7
8
from sqlmodel import SQLModel, create_engine, Session

DATABASE_URL = "sqlite:///database.db"
engine = create_engine(DATABASE_URL, echo=True)

def get_session():
    with Session(engine) as session:
        yield session
🔥Engine Configuration
📊 Production Insight
In production, use a connection pool with pool_size=10 and max_overflow=20 to handle concurrent requests.
🎯 Key Takeaway
SQLModel uses SQLAlchemy's engine and session under the hood. You can configure connection pools and other options via create_engine.

Defining Models

Models are defined by inheriting from SQLModel. Each attribute is a column. Use type hints for validation. Example: a Hero model with id, name, secret_name, and age.

models.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
from typing import Optional
from sqlmodel import SQLModel, Field

class Hero(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: Optional[int] = None

# Create all tables
SQLModel.metadata.create_all(engine)
💡Table vs Non-Table Models
📊 Production Insight
Always set id as Optional[int] with default None so SQLAlchemy auto-generates it. For UUIDs, use Field(default_factory=uuid.uuid4).
🎯 Key Takeaway
A single class defines both the database table and the API schema. Use Field for column constraints like primary_key, nullable, etc.

CRUD Operations

CRUD (Create, Read, Update, Delete) is straightforward with sessions. Here's how to create and read heroes:

crud.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from sqlmodel import Session, select

# Create
with Session(engine) as session:
    hero = Hero(name="Deadpond", secret_name="Dive Wilson", age=25)
    session.add(hero)
    session.commit()
    session.refresh(hero)
    print(hero.id)  # 1

# Read
with Session(engine) as session:
    statement = select(Hero).where(Hero.name == "Deadpond")
    result = session.exec(statement)
    hero = result.one()
    print(hero.name)  # Deadpond
Output
1
Deadpond
⚠ Always Commit
📊 Production Insight
Wrap CRUD operations in try-except to handle IntegrityError and rollback on failure. Use session.rollback() to reset the session.
🎯 Key Takeaway
Use session.exec() for queries (returns a Result object) and session.add() for inserts. Always commit and refresh to get generated IDs.

Relationships: One-to-Many

SQLModel supports relationships via Relationship and foreign keys. Example: Team has many Heroes.

relationships.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from typing import Optional, List
from sqlmodel import SQLModel, Field, Relationship

class Team(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
    headquarters: str
    heroes: List["Hero"] = Relationship(back_populates="team")

class Hero(SQLModel, table=True):
    id: Optional[int] = Field(default=None, primary_key=True)
    name: str
    secret_name: str
    age: Optional[int] = None
    team_id: Optional[int] = Field(default=None, foreign_key="team.id")
    team: Optional[Team] = Relationship(back_populates="heroes")
🔥Back Populates
📊 Production Insight
For large datasets, use selectinload to eagerly load relationships and avoid N+1 queries: statement = select(Team).options(selectinload(Team.heroes)).
🎯 Key Takeaway
Define foreign keys with Field(foreign_key="table.column") and use Relationship to navigate between models.

Using SQLModel with FastAPI

SQLModel shines in FastAPI. Define request/response models as non-table models, and database models as table=True. Use dependency injection for sessions.

fastapi_app.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from fastapi import FastAPI, Depends
from sqlmodel import Session, select

app = FastAPI()

# Non-table model for request
class HeroCreate(SQLModel):
    name: str
    secret_name: str
    age: Optional[int] = None

@app.post("/heroes/")
def create_hero(hero: HeroCreate, session: Session = Depends(get_session)):
    db_hero = Hero.from_orm(hero)
    session.add(db_hero)
    session.commit()
    session.refresh(db_hero)
    return db_hero

@app.get("/heroes/")
def read_heroes(session: Session = Depends(get_session)):
    heroes = session.exec(select(Hero)).all()
    return heroes
💡from_orm vs model_validate
📊 Production Insight
Add response_model to endpoints to automatically filter fields. For example, response_model=HeroRead where HeroRead excludes secret fields.
🎯 Key Takeaway
Use non-table models for input/output to avoid exposing internal fields. Use from_orm or model_validate to convert.

Migrations with Alembic

SQLModel works with Alembic for database migrations. Install alembic and configure it to use your SQLModel metadata.

alembic_env.pyPYTHON
1
2
3
4
from sqlmodel import SQLModel
from models import Hero, Team  # import all models

target_metadata = SQLModel.metadata
⚠ Import All Models
📊 Production Insight
Always review auto-generated migrations before applying. They may miss complex changes like column type alterations.
🎯 Key Takeaway
Set target_metadata = SQLModel.metadata in Alembic. Run alembic revision --autogenerate -m "message" to generate migrations.

Async Support

SQLModel supports async with databases like PostgreSQL. Use AsyncSession and create_async_engine.

async_example.pyPYTHON
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from sqlmodel import SQLModel, create_async_engine, select
from sqlmodel.ext.asyncio.session import AsyncSession

DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/db"
engine = create_async_engine(DATABASE_URL, echo=True)

async def get_session():
    async with AsyncSession(engine) as session:
        yield session

async def create_hero(hero: HeroCreate):
    async with AsyncSession(engine) as session:
        db_hero = Hero.from_orm(hero)
        session.add(db_hero)
        await session.commit()
        await session.refresh(db_hero)
        return db_hero
🔥Async Drivers
📊 Production Insight
Async sessions are not thread-safe. Use asyncio.Lock if sharing sessions across tasks.
🎯 Key Takeaway
Use create_async_engine and AsyncSession for async operations. All session methods become awaitable.
● Production incidentPOST-MORTEMseverity: high

The Missing Foreign Key: A Production Outage

Symptom
Users reported 500 errors when creating orders. Logs showed 'IntegrityError: NOT NULL constraint failed'.
Assumption
The developer assumed that adding a new optional field wouldn't break existing code.
Root cause
A new foreign key column was added with nullable=False, but existing rows had no corresponding parent record, violating the constraint.
Fix
Changed the column to nullable=True, then ran a migration to backfill missing references, then set nullable=False.
Key lesson
  • Always set new foreign keys as nullable initially in production.
  • Use database migrations to apply schema changes safely.
  • Test schema changes against a copy of production data.
  • Add database constraint checks in CI/CD pipelines.
  • Monitor error rates after deployments to catch regressions early.
Production debug guideSymptom to Action4 entries
Symptom · 01
IntegrityError on insert
Fix
Check foreign key constraints and nullable settings. Use session.rollback() and log the failing data.
Symptom · 02
Slow queries
Fix
Enable SQLAlchemy echo=True to log queries. Add indexes on frequently filtered columns.
Symptom · 03
Model not found in API docs
Fix
Ensure model inherits from SQLModel and is imported in the app. Check that response_model is set correctly.
Symptom · 04
Session not closed
Fix
Use dependency injection with yield to ensure session is closed after request.
★ Quick Debug Cheat SheetCommon SQLModel issues and immediate actions.
IntegrityError: NOT NULL constraint failed
Immediate action
Check if new column is nullable=False and existing rows have NULL.
Commands
SELECT COUNT(*) FROM table WHERE new_column IS NULL;
ALTER TABLE table ALTER COLUMN new_column DROP NOT NULL;
Fix now
Set nullable=True in model, then backfill data.
No response from API after DB change+
Immediate action
Check if session is properly committed.
Commands
print(session.dirty) # check pending changes
session.commit()
Fix now
Ensure commit() is called after each write operation.
FastAPI docs show wrong schema+
Immediate action
Verify model inherits from SQLModel and response_model is correct.
Commands
print(model.__fields__) # Pydantic fields
print(model.__table__) # SQLAlchemy table
Fix now
Add response_model=ModelName to endpoint decorator.
FeatureSQLAlchemyPydanticSQLModel
Define modelsSeparate classesSeparate classesSingle class
ValidationManualAutomaticAutomatic
FastAPI integrationRequires extra codeBuilt-inSeamless
Async supportVia extensionsN/ABuilt-in
⚙ Quick Reference
7 commands from this guide
FileCommand / CodePurpose
setup.pyfrom sqlmodel import SQLModel, create_engine, SessionSetting Up SQLModel
models.pyfrom typing import OptionalDefining Models
crud.pyfrom sqlmodel import Session, selectCRUD Operations
relationships.pyfrom typing import Optional, ListRelationships
fastapi_app.pyfrom fastapi import FastAPI, DependsUsing SQLModel with FastAPI
alembic_env.pyfrom sqlmodel import SQLModelMigrations with Alembic
async_example.pyfrom sqlmodel import SQLModel, create_async_engine, selectAsync Support

Key takeaways

1
SQLModel merges SQLAlchemy and Pydantic into one model, reducing boilerplate.
2
Use table=True for database models and non-table models for API schemas.
3
Always commit sessions and handle exceptions with rollback.
4
Use Alembic for migrations and eager loading to avoid N+1 queries.
5
SQLModel works seamlessly with FastAPI, especially with dependency injection for sessions.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
How does SQLModel combine SQLAlchemy and Pydantic?
Q02JUNIOR
What is the purpose of `table=True` in SQLModel?
Q03SENIOR
How do you prevent N+1 queries in SQLModel?
Q01 of 03SENIOR

How does SQLModel combine SQLAlchemy and Pydantic?

ANSWER
SQLModel uses Python's type hints to define both SQLAlchemy columns and Pydantic fields. At runtime, it generates the SQLAlchemy table and Pydantic schema from the same class.
FAQ · 3 QUESTIONS

Frequently Asked Questions

01
What is the difference between SQLModel and SQLAlchemy?
02
Can I use SQLModel without FastAPI?
03
How do I handle many-to-many relationships in SQLModel?
N
Naren Founder & Principal Engineer

20+ years shipping production Python across data and backend systems. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 15, 2026
last updated
2,398
articles · all by Naren
🔥

That's Python Libraries. Mark it forged?

3 min read · try the examples if you haven't

Previous
GraphQL in Python with Strawberry
65 / 69 · Python Libraries
Next
Django ORM Deep-Dive: Querysets, Aggregation, and Performance