SQLModel

Updated 5 interview angles 3 min read source
On this page5
  1. What it actually does
  2. The session is SQLAlchemy’s
  3. Where it helps and where it hurts
  4. Related
  5. Interview angle

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.

python
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: str

table=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.

python
class HeroPublic(SQLModel):     # no table
    id: int
    name: str          # `secret` deliberately absent

That 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:

python
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.

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=True do?” - 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.exec versus session.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.