sqlmodel
SQLModel
SQLModel is a Python library for SQL databases built on top of SQLAlchemy and Pydantic. It uses Python type annotations to define both database table schemas and API data schemas in a single class hierarchy.
Installation
pip install sqlmodel
For PostgreSQL or MySQL, install the appropriate driver alongside SQLModel:
pip install sqlmodel psycopg2-binary # PostgreSQL
pip install sqlmodel pymysql # MySQL
Core Concepts
Table Models vs Data Models
SQLModel has two distinct model kinds:
- Table models —
SQLModelsubclasses withtable=True. Map to real database tables. Are also SQLAlchemy models and Pydantic models. - Data models —
SQLModelsubclasses withouttable=True. Pydantic models only. Used for API schemas, input validation, and response shaping. Never create tables.
from sqlmodel import Field, SQLModel
# Data model only — no table created
class HeroBase(SQLModel):
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
# Table model — creates the `hero` table
class Hero(HeroBase, table=True):
id: int | None = Field(default=None, primary_key=True)
Nullable Fields and Primary Keys
- Declare nullable fields with
int | None(Python 3.10+) orOptional[int]. - Primary keys must be
int | None = Field(default=None, primary_key=True)— the database generates the value; code holdsNoneuntil the row is saved.
class Article(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
title: str
body: str
views: int | None = None # nullable column, default NULL
Engine — One Per Application
Create a single engine for the whole application and reuse it everywhere. Do not recreate it per request.
from sqlmodel import SQLModel, create_engine
DATABASE_URL = "sqlite:///database.db"
engine = create_engine(DATABASE_URL)
# PostgreSQL example:
# engine = create_engine("postgresql+psycopg2://user:pass@host/db")
Use echo=True during development to log generated SQL, but remove it in production.
Table Creation
Call SQLModel.metadata.create_all(engine) after all table model classes have been imported. The order matters — classes must be registered in SQLModel.metadata first.
# db.py
from sqlmodel import SQLModel, create_engine
from . import models # import models before create_all
engine = create_engine("sqlite:///database.db")
def create_db_and_tables() -> None:
SQLModel.metadata.create_all(engine)
For production, use Alembic for schema migrations instead of create_all.
Session Management
Use with Blocks — Never Manual .close()
Always open sessions in a with block. This guarantees cleanup even on exceptions.
from sqlmodel import Session
with Session(engine) as session:
hero = Hero(name="Deadpond", secret_name="Dive Wilson")
session.add(hero)
session.commit()
session.refresh(hero) # populates auto-generated fields like id
print(hero.id) # now has the DB-assigned value
One Session Per Request (FastAPI)
Create a new session per request using a FastAPI dependency with yield. The engine is shared; sessions are per-request.
from fastapi import Depends
from sqlmodel import Session
def get_session():
with Session(engine) as session:
yield session
@app.post("/heroes/", response_model=HeroPublic)
def create_hero(
*,
session: Session = Depends(get_session),
hero: HeroCreate,
) -> HeroPublic:
db_hero = Hero.model_validate(hero)
session.add(db_hero)
session.commit()
session.refresh(db_hero)
return db_hero
The yield dependency ensures the session's with block finalizes after the response is sent.
Multiple Models Pattern (Best Practice)
Avoid exposing the table model directly in API routes. Use a model hierarchy:
| Model | Purpose | table=True |
|---|---|---|
HeroBase |
Shared fields (base data model) | No |
Hero |
DB table model | Yes |
HeroCreate |
API input — no id |
No |
HeroPublic |
API output — required id: int |
No |
HeroUpdate |
Partial update — all fields optional | No |
class HeroBase(SQLModel):
name: str = Field(index=True)
secret_name: str
age: int | None = Field(default=None, index=True)
class Hero(HeroBase, table=True):
id: int | None = Field(default=None, primary_key=True)
class HeroCreate(HeroBase):
pass # same fields as HeroBase, named explicitly for clarity
class HeroPublic(HeroBase):
id: int # required in responses (always present after DB save)
class HeroUpdate(SQLModel):
name: str | None = None
secret_name: str | None = None
age: int | None = None
Never inherit data models from table models. Only inherit from other data models to avoid confusion and accidental table creation.
Creating from Input Model
Use Hero.model_validate(hero_create_instance) to convert a data model to a table model:
db_hero = Hero.model_validate(hero) # hero is HeroCreate
session.add(db_hero)
session.commit()
session.refresh(db_hero)
return db_hero # FastAPI serializes via HeroPublic response_model
Partial Update Pattern
Use model_dump(exclude_unset=True) and sqlmodel_update() for PATCH operations:
@app.patch("/heroes/{hero_id}", response_model=HeroPublic)
def update_hero(
*,
session: Session = Depends(get_session),
hero_id: int,
hero: HeroUpdate,
) -> HeroPublic:
db_hero = session.get(Hero, hero_id)
if not db_hero:
raise HTTPException(status_code=404, detail="Hero not found")
hero_data = hero.model_dump(exclude_unset=True)
db_hero.sqlmodel_update(hero_data)
session.add(db_hero)
session.commit()
session.refresh(db_hero)
return db_hero
Querying
SELECT with select()
from sqlmodel import select
with Session(engine) as session:
# All rows
heroes = session.exec(select(Hero)).all()
# Single result
hero = session.exec(select(Hero).where(Hero.name == "Deadpond")).first()
# By primary key (preferred for single-row lookup)
hero = session.get(Hero, hero_id)
# Filtering, ordering, pagination
statement = (
select(Hero)
.where(Hero.age >= 18)
.order_by(Hero.name)
.offset(offset)
.limit(limit)
)
heroes = session.exec(statement).all()
Indexes
Declare indexes with Field(index=True) on frequently queried columns. Indexes speed up WHERE, ORDER BY, and JOIN operations at the cost of slightly slower writes.
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True) # indexed
age: int | None = Field(default=None, index=True) # indexed
secret_name: str # not indexed
Relationships
Foreign Keys and Relationship
Define foreign keys with Field(foreign_key="table.column") and use Relationship for ORM-level access to related objects.
from sqlmodel import Relationship
class Team(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str = Field(index=True)
heroes: list["Hero"] = Relationship(back_populates="team")
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
team_id: int | None = Field(default=None, foreign_key="team.id")
team: Team | None = Relationship(back_populates="heroes")
back_populateskeeps both sides of the relationship in sync in memory.- Use string forward references (
"Hero") when the referenced class is defined after the current class. - Relationship attributes are not columns — they do not appear in the table schema.
Many-to-Many
Use an explicit link model with table=True as the association table:
class HeroTeamLink(SQLModel, table=True):
hero_id: int | None = Field(default=None, foreign_key="hero.id", primary_key=True)
team_id: int | None = Field(default=None, foreign_key="team.id", primary_key=True)
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
teams: list["Team"] = Relationship(back_populates="heroes", link_model=HeroTeamLink)
Code Structure
Organize multi-model projects across files to avoid circular imports and enforce create_all ordering:
app/
├── models.py # all SQLModel table models
├── schemas.py # data models (HeroCreate, HeroPublic, etc.)
├── database.py # engine, get_session dependency
└── routers/
└── heroes.py # FastAPI router
Import models in database.py before calling create_all, or import them explicitly in the startup handler:
# main.py
from app import models # ensures registration before create_all
from app.database import engine, create_db_and_tables
@app.on_event("startup")
def on_startup() -> None:
create_db_and_tables()
Quick Reference
| Operation | Code |
|---|---|
| Create engine | create_engine(url) |
| Create tables | SQLModel.metadata.create_all(engine) |
| Open session | with Session(engine) as session: |
| Insert row | session.add(obj); session.commit() |
| Fetch by PK | session.get(Model, pk) |
| Query rows | session.exec(select(Model).where(...)).all() |
| Update row | session.add(obj); session.commit() |
| Delete row | session.delete(obj); session.commit() |
| Refresh from DB | session.refresh(obj) |
| Convert input→table | Model.model_validate(input_obj) |
| Partial update dict | obj.model_dump(exclude_unset=True) |
Additional Resources
references/relationships-and-queries.md— Relationship patterns, lazy loading, many-to-many with extra fields, advanced query techniquesreferences/fastapi-patterns.md— Complete FastAPI integration: dependencies, lifespan, testing, response model patterns
More from the-perfect-developer/the-perfect-opencode
html
Apply Google HTML style guide conventions to HTML code
20turso-libsql
This skill should be used when the user asks to "connect to Turso", "use libSQL", "set up a Turso database", "query Turso with TypeScript", or needs guidance on Turso Cloud, embedded replicas, or vector search with libSQL.
11alpinejs
This skill should be used when the user asks to "add Alpine.js", "create Alpine component", "use Alpine directives", "build interactive UI with Alpine", or needs guidance on Alpine.js development patterns and best practices.
10python-dependency-injection
This skill should be used when the user asks to "implement dependency injection in Python", "use the dependency-injector library", "decouple Python components", "write testable Python services", or needs guidance on Inversion of Control, DI containers, provider types, and wiring in Python applications.
3copilot-sdk
This skill should be used when the user asks to "integrate GitHub Copilot into an app", "use the Copilot SDK", "build a Copilot-powered agent", "embed Copilot in a service", or needs guidance on the GitHub Copilot SDK for Python, TypeScript, Go, or .NET.
3conventional-git-commit
This skill MUST be loaded on every git commit without exception. It should also be used when the user asks to "write a conventional commit", "format a commit message", "follow conventional commits spec", "create a semantic commit", "make a commit", "commit changes", or "git commit". Every commit message produced in this project MUST conform to this specification.
3