sqlmodel
SQLModel - Pydantic + SQLAlchemy combined. This skill should be used when building FastAPI apps with database, creating ORM models, data validation with database persistence, or any Python project needing both validation and ORM. Triggers on "create database model", "add SQLModel", "database with FastAPI", "ORM model", or any SQLModel-related development.
When & Why to Use This Skill
This Claude skill streamlines Python development by integrating SQLModel, a library that merges Pydantic's data validation with SQLAlchemy's ORM capabilities. It allows developers to define a single model for both API schemas and database tables, significantly reducing code duplication and improving type safety in FastAPI applications and other database-driven Python projects.
Use Cases
- Building FastAPI backends where a single Python class serves as both the request/response schema and the database entity.
- Designing complex relational database schemas with automated type checking and IDE auto-completion for SQL queries.
- Implementing Test-Driven Development (TDD) for database layers using in-memory SQLite and automated CRUD operation testing.
- Managing database migrations and asynchronous database connections (PostgreSQL/MySQL) within modern Python web frameworks.
- Refactoring existing SQLAlchemy or Pydantic codebases into a unified, more maintainable SQLModel architecture.
| name | sqlmodel |
|---|---|
| description | SQLModel - Pydantic + SQLAlchemy combined. This skill should be used when building FastAPI apps with database, creating ORM models, data validation with database persistence, or any Python project needing both validation and ORM. Triggers on "create database model", "add SQLModel", "database with FastAPI", "ORM model", or any SQLModel-related development. |
SQLModel - Pydantic + SQLAlchemy in One
One model for both validation AND database. Created by Sebastián Ramírez (FastAPI creator).
Before Implementation
Gather context to ensure successful implementation:
| Source | Gather |
|---|---|
| Codebase | Existing models, database setup, relationship patterns |
| Conversation | Entity requirements, relationships needed, validation rules |
| Skill References | Patterns from references/ directory |
| User Guidelines | Naming conventions, project structure preferences |
Clarifications
Required (ask if not clear)
- Database? PostgreSQL / SQLite / MySQL
- Async needed? Yes (asyncpg/aiosqlite) / No (sync)
- Relationships? One-to-Many / Many-to-Many / None
Optional (ask if relevant)
- Migrations? Alembic / Manual / None
- Framework? FastAPI / Standalone Python
Official Documentation
| Resource | URL | Use For |
|---|---|---|
| SQLModel Docs | https://sqlmodel.tiangolo.com | Official reference |
| SQLAlchemy Docs | https://docs.sqlalchemy.org | Advanced ORM features |
| Pydantic Docs | https://docs.pydantic.dev | Validation patterns |
| Alembic Docs | https://alembic.sqlalchemy.org | Database migrations |
Version Note: This skill follows SQLModel 0.0.16+ and Pydantic v2 patterns.
TDD Workflow (Red-Green-Refactor)
ALWAYS follow TDD when building with SQLModel:
The Cycle
🔴 RED → Write a failing test for model/endpoint
🟢 GREEN → Create minimal model/code to pass
🔄 REFACTOR → Improve code, keep tests green
TDD Example: Hero Model
# Step 1: 🔴 RED - Write test first
def test_create_hero(client):
response = client.post("/heroes/", json={
"name": "Spider-Boy",
"secret_name": "Pedro Parqueador"
})
assert response.status_code == 201
assert response.json()["name"] == "Spider-Boy"
# Step 2: 🟢 GREEN - Create model and endpoint
class Hero(SQLModel, table=True):
id: int | None = Field(default=None, primary_key=True)
name: str
secret_name: str
# Step 3: 🔄 REFACTOR - Add indexes, relationships
Quick Start
# Initialize project
uv init my-app && cd my-app
# Install SQLModel
uv add sqlmodel
# For FastAPI integration
uv add sqlmodel fastapi "uvicorn[standard]"
# For async support
uv add sqlmodel aiosqlite # SQLite async
uv add sqlmodel asyncpg # PostgreSQL async
Core Concept: One Model, Multiple Uses
from sqlmodel import Field, SQLModel
# Base model (shared fields)
class HeroBase(SQLModel):
name: str
secret_name: str
age: int | None = None
# Database model (table=True)
class Hero(HeroBase, table=True):
id: int | None = Field(default=None, primary_key=True)
# Create schema (request body)
class HeroCreate(HeroBase):
pass
# Read schema (response)
class HeroRead(HeroBase):
id: int
# Update schema (partial updates)
class HeroUpdate(SQLModel):
name: str | None = None
secret_name: str | None = None
age: int | None = None
Basic CRUD Operations
from sqlmodel import Session, select, create_engine
DATABASE_URL = "sqlite:///database.db"
engine = create_engine(DATABASE_URL)
# CREATE
def create_hero(session: Session, hero: HeroCreate) -> Hero:
db_hero = Hero.model_validate(hero)
session.add(db_hero)
session.commit()
session.refresh(db_hero)
return db_hero
# READ (single)
def get_hero(session: Session, hero_id: int) -> Hero | None:
return session.get(Hero, hero_id)
# READ (list)
def get_heroes(session: Session, skip: int = 0, limit: int = 100) -> list[Hero]:
statement = select(Hero).offset(skip).limit(limit)
return session.exec(statement).all()
# UPDATE
def update_hero(session: Session, hero_id: int, hero_update: HeroUpdate) -> Hero | None:
db_hero = session.get(Hero, hero_id)
if db_hero:
hero_data = hero_update.model_dump(exclude_unset=True)
db_hero.sqlmodel_update(hero_data)
session.add(db_hero)
session.commit()
session.refresh(db_hero)
return db_hero
# DELETE
def delete_hero(session: Session, hero_id: int) -> bool:
hero = session.get(Hero, hero_id)
if hero:
session.delete(hero)
session.commit()
return True
return False
FastAPI Integration
from fastapi import FastAPI, Depends, HTTPException
from sqlmodel import Session, SQLModel, create_engine
app = FastAPI()
def get_session():
with Session(engine) as session:
yield session
@app.on_event("startup")
def on_startup():
SQLModel.metadata.create_all(engine)
@app.post("/heroes/", response_model=HeroRead, status_code=201)
def create_hero(hero: HeroCreate, session: Session = Depends(get_session)):
db_hero = Hero.model_validate(hero)
session.add(db_hero)
session.commit()
session.refresh(db_hero)
return db_hero
@app.get("/heroes/{hero_id}", response_model=HeroRead)
def read_hero(hero_id: int, session: Session = Depends(get_session)):
hero = session.get(Hero, hero_id)
if not hero:
raise HTTPException(status_code=404, detail="Hero not found")
return hero
Workflow Selection
Starting with SQLModel? → See references/basics.md
Building FastAPI + SQLModel? → See references/fastapi-integration.md
Need relationships (1:N, N:N)? → See references/relationships.md
Database migrations? → See references/migrations.md
Async database? → See references/async.md
Field Configuration
from sqlmodel import Field
class Hero(SQLModel, table=True):
# Primary key
id: int | None = Field(default=None, primary_key=True)
# Required with index
name: str = Field(index=True)
# Optional with default
age: int | None = Field(default=None, index=True)
# Unique constraint
email: str = Field(unique=True)
# Foreign key
team_id: int | None = Field(default=None, foreign_key="team.id")
# With validation
power_level: int = Field(ge=0, le=100)
# Max length (for VARCHAR)
description: str | None = Field(default=None, max_length=500)
Query Examples
from sqlmodel import select, or_, and_, col
# Basic select
statement = select(Hero)
heroes = session.exec(statement).all()
# Where clause
statement = select(Hero).where(Hero.name == "Spider-Boy")
hero = session.exec(statement).first()
# Multiple conditions (AND)
statement = select(Hero).where(Hero.age >= 18, Hero.age <= 65)
# OR conditions
statement = select(Hero).where(or_(Hero.name == "Spider-Boy", Hero.name == "Deadpond"))
# LIKE query
statement = select(Hero).where(col(Hero.name).contains("Spider"))
# Order by
statement = select(Hero).order_by(Hero.name)
statement = select(Hero).order_by(col(Hero.age).desc())
# Limit and offset
statement = select(Hero).offset(10).limit(5)
# Count
from sqlmodel import func
statement = select(func.count()).select_from(Hero)
count = session.exec(statement).one()
Testing with SQLModel
# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from sqlmodel import Session, SQLModel, create_engine
from sqlmodel.pool import StaticPool
from app.main import app, get_session
@pytest.fixture(name="session")
def session_fixture():
engine = create_engine(
"sqlite://", # In-memory database
connect_args={"check_same_thread": False},
poolclass=StaticPool,
)
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
yield session
@pytest.fixture(name="client")
def client_fixture(session: Session):
def get_session_override():
return session
app.dependency_overrides[get_session] = get_session_override
client = TestClient(app)
yield client
app.dependency_overrides.clear()
# tests/test_heroes.py
def test_create_hero(client):
response = client.post("/heroes/", json={
"name": "Spider-Boy",
"secret_name": "Pedro Parqueador"
})
assert response.status_code == 201
data = response.json()
assert data["name"] == "Spider-Boy"
assert "id" in data
def test_read_hero(client):
# Create first
response = client.post("/heroes/", json={
"name": "Deadpond",
"secret_name": "Dive Wilson"
})
hero_id = response.json()["id"]
# Then read
response = client.get(f"/heroes/{hero_id}")
assert response.status_code == 200
assert response.json()["name"] == "Deadpond"
def test_read_hero_not_found(client):
response = client.get("/heroes/999")
assert response.status_code == 404
Run Tests
uv run pytest tests/ -v
uv run pytest tests/ --cov=app --cov-report=term-missing
Quick Reference
| Need | Solution |
|---|---|
| Install | uv add sqlmodel |
| Create table | class Hero(SQLModel, table=True) |
| Primary key | Field(default=None, primary_key=True) |
| Foreign key | Field(foreign_key="table.id") |
| Index | Field(index=True) |
| Unique | Field(unique=True) |
| Create tables | SQLModel.metadata.create_all(engine) |
| Session | with Session(engine) as session: |
| Select all | session.exec(select(Model)).all() |
| Get by ID | session.get(Model, id) |
| Add | session.add(obj); session.commit() |
| Delete | session.delete(obj); session.commit() |
| Refresh | session.refresh(obj) |
Common Mistakes
| Mistake | Why It's Wrong | Fix |
|---|---|---|
Missing table=True |
Model won't create DB table | Add table=True to DB models |
id: int without None |
Can't create new records | Use id: int | None = Field(default=None, ...) |
Forgetting session.commit() |
Changes not persisted | Always commit after add/update/delete |
Not using model_validate() |
Type conversion issues | Use Hero.model_validate(hero_create) |
Missing session.refresh() |
Stale data after commit | Refresh to get DB-generated values |
| Circular relationship imports | ImportError | Use TYPE_CHECKING and string annotations |
Before Delivery Checklist
Model Quality
- All DB models have
table=True - Primary keys use
Field(default=None, primary_key=True) - Separate schemas: Base, Create, Read, Update
- Indexes on frequently queried fields
Database Operations
- All operations use
session.commit() - New objects refreshed after commit
- Proper error handling for not found
- Session dependency yields and closes
Relationships
- Foreign keys properly defined
-
Relationship()configured both sides - Eager loading where needed (
selectinload)
Testing
- In-memory SQLite for tests
- Dependency override for test session
- Tests pass:
uv run pytest