SQLModel
SQLModel is a thin layer that makes one class serve as both a SQLAlchemy table and a Pydantic model. It comes from the FastAPI author, and job descriptions often write “SQLAlchemy / SQLModel” as if they were alternatives. They are not - SQLModel is SQLAlchemy underneath.
What it actually does
The duplication it targets is real. Without it, a FastAPI endpoint typically has an ORM model and one or more Pydantic schemas describing the same fields.
from sqlmodel import SQLModel, Field
class Hero(SQLModel, table=True):
id: int | None = Field(
default=None, primary_key=True
)
name: str = Field(index=True)
secret: strtable=True is the switch. With it, the class is a mapped SQLAlchemy table.
Without it, the class is an ordinary Pydantic model - which is how you build
request and response schemas in the same file and same style.
class HeroPublic(SQLModel): # no table
id: int
name: str # `secret` deliberately absentThat pattern - a table class plus non-table classes for input and output - is the intended design, and it matters: returning the table class directly from an endpoint exposes every column, including the ones you did not mean to publish.
The session is SQLAlchemy’s
Everything below the model layer is unchanged, which is the useful thing to know in an interview:
from sqlmodel import Session, select
with Session(engine) as session:
heroes = session.exec(
select(Hero).where(Hero.name == "Deadpond")
).all()select() is SQLAlchemy 2.0’s select. The engine is SQLAlchemy’s engine.
Alembic migrates it the same way. session.exec() is SQLModel’s small wrapper
over session.execute() that unwraps single-entity rows so you get Hero
objects rather than one-element tuples.
Async works through the same SQLAlchemy machinery -
AsyncSession and create_async_engine - so
Async SQLAlchemy applies unchanged.
Where it helps and where it hurts
| Helps | Small services, one shape per entity, fast start |
| Hurts | Divergent API and storage shapes, deep ORM work |
It helps when the API representation and the table are genuinely the same shape. You write fields once, get editor completion and validation, and the FastAPI integration is seamless because the response model is already Pydantic.
It hurts the moment those shapes diverge, which they do as a service grows. A column renamed for storage reasons should not rename the API field; a computed API field has no column. You end up writing the separate classes you were avoiding, plus the coupling of having tried not to.
The other cost is ecosystem depth. Advanced SQLAlchemy - polymorphic inheritance, hybrid properties, custom types, complex relationship configuration - is documented for SQLAlchemy, and SQLModel’s own documentation stops well short of it. You will read SQLAlchemy docs and translate.
The honest recommendation
Use SQLModel for a small or short-lived service where the shapes match. Use SQLAlchemy with explicit Pydantic schemas when the service will grow, because the separation you are avoiding is the one that stops a storage change from becoming an API change.
Either answer is defensible in an interview provided you name that trade rather than treating SQLModel as strictly newer and therefore better.
Related
Interview angle 5
- “SQLAlchemy or SQLModel?” - not alternatives. SQLModel is a thin layer over SQLAlchemy that also makes the class a Pydantic model, so the engine, session,
select()and Alembic are all still SQLAlchemy. - “What does
table=Truedo?” - marks the class as a mapped table. Without it the same class is a plain Pydantic model, which is how you write request and response schemas in the same style. - “What is the risk of one class for both?” - returning the table class from an endpoint publishes every column, including secrets. The intended pattern is a table class plus separate non-table classes for input and output, which is most of the duplication back again.
- “When does it stop paying off?” - when the API shape and the storage shape diverge, which growth causes. Then a storage-driven rename becomes an API change, and you write the separate schemas anyway.
- “What is
session.execversussession.execute?” - SQLModel’s wrapper that unwraps single-entity results, so you get model objects rather than one-element tuples. Underneath it is the same SQLAlchemy call.