# SQLModel: one Python class for the table and the API schema

> SQLModel is a thin layer over Pydantic and SQLAlchemy that lets a single typed class describe both a database table and a request body. It fits FastAPI projects that want less duplication, and it is still labelled Beta at 0.0.42.

**fastapi/sqlmodel** — SQL databases in Python, designed for simplicity, compatibility, and robustness.

- Repository: https://github.com/fastapi/sqlmodel
- Website: https://sqlmodel.tiangolo.com/
- Stars: 18,350 · Forks: 891
- Language: Python
- License: MIT
- Published: 2026-09-21 · Updated: 2026-09-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/fastapi-sqlmodel

## The duplication problem SQLModel removes

A typical FastAPI service ends up with two descriptions of the same thing. One is the SQLAlchemy model that maps to the table. The other is the Pydantic model that validates the request body and shapes the JSON response. They carry the same field names, the same types, and the same optionality, and they drift apart the moment someone adds a column and forgets the second class.

SQLModel collapses that pair into one class. The README states the goal plainly: "No need to duplicate models in SQLAlchemy and Pydantic." A class that inherits from SQLModel and sets table=True is registered as a SQL table, while the same type annotations that describe the columns also describe the validation rules. The audience is narrow and specific: Python developers building HTTP services with FastAPI who are already comfortable with type annotations and who want the database layer to stop being a separate vocabulary.

It is not aimed at data engineers doing analytical SQL, and it is not a query builder for people who prefer raw SQL strings. The README frames it as a library for interacting with SQL databases "from Python code, with Python objects."

## How the SQLModel layer sits between Pydantic and SQLAlchemy

The README calls SQLModel "a thin layer on top of Pydantic and SQLAlchemy, carefully designed to be compatible with both." That sentence is the whole architecture. There is no separate storage engine and no query language of its own.

When you declare a class with table=True, SQLModel reads the annotations and the Field() defaults, builds a SQLAlchemy Table in SQLModel.metadata, and keeps the Pydantic validation behaviour on the same class. Queries go through select() and a Session, both re-exported from SQLModel, and both ultimately SQLAlchemy objects. The pyproject file pins the dependency as SQLAlchemy >=2.0.14,<2.1.0 and pydantic>=2.11.0, so the layer tracks those two libraries rather than replacing them.

That design has a visible consequence. Because the layer is thin, anything SQLAlchemy can express and SQLModel has not wrapped is still reachable, which is why the search data around this project keeps returning terms like sa_column and sa_type: those are the escape hatches back to the underlying column definition. The trade-off is that the abstraction leaks by design. You get shorter model code, and in exchange you are expected to know what is underneath when the wrapper runs out.

## Installing SQLModel and writing the first row

The README's installation path goes through uv. Install uv first, then add the package to your project, which resolves SQLAlchemy, Pydantic and typing-extensions automatically because they are declared dependencies.

```bash
uv add sqlmodel
```

The README notes that if you prefer pip, you should install sqlmodel inside a virtual environment and points to its installation guide for those steps. Python 3.10 or newer is required according to pyproject.

The next block is the README's own end-to-end example. It defines a Hero table, creates three instances, opens a SQLite file called database.db, creates the tables from the metadata, and commits the rows inside a Session.

```python
from sqlmodel import Field, Session, SQLModel, create_engine


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


engine = create_engine("sqlite:///database.db")

SQLModel.metadata.create_all(engine)
```

After running it you should have a database.db file in the working directory containing a hero table. Reading back is a select() statement executed through session.exec, as the README's follow-up example shows with select(Hero).where(Hero.name == "Spider-Boy"). To point the same code at another database, change the URL passed to create_engine; the README does not walk through a Postgres URL, so treat that as a step you verify against SQLAlchemy's own documentation.

## Where SQLModel stops being the right tool

The version number is the first thing to weigh. The latest release listed is 0.0.42, and the pyproject classifier reads "Development Status :: 4 - Beta". A 0.0.x line signals that the maintainers have not made a compatibility promise, so a minor bump can change behaviour. If your project needs a frozen API surface across years, this is a poor fit regardless of how pleasant the model code looks.

Migrations are the second gap. The README shows SQLModel.metadata.create_all(engine), which creates tables that do not exist yet. It does not alter existing tables. There is no migration engine in the README, and the search data around the project shows people asking how to use Alembic with SQLModel, which tells you the answer is not in the main tutorial. In practice you generate migrations from the SQLAlchemy metadata that SQLModel builds, and you own that pipeline yourself.

The third limit is the escape hatch. The moment your schema needs a column type, a server default, or a constraint the wrapper does not expose, you drop to sa_column and are writing SQLAlchemy inside a SQLModel class. At that point the duplication you removed comes back as a mix of two APIs in one file. Teams with complex schemas often find plain SQLAlchemy clearer than a half-used abstraction, and that is a legitimate reason to skip this library.

## SQLModel vs SQLAlchemy: what actually differs

The honest comparison is not feature against feature, because SQLModel depends on SQLAlchemy and cannot do anything SQLAlchemy cannot. The difference is where the type information lives.

In SQLAlchemy you declare columns as class attributes assigned to Column or Mapped with mapped_column, and validation is a separate concern handled by Pydantic models you write yourself. In SQLModel the annotation is the column and the validator at once, and the same class can be handed to FastAPI as a response model without a second definition. That is the entire value proposition, and it is real for CRUD-shaped services with straightforward schemas.

The cost is indirection. When something goes wrong at the database boundary, the traceback passes through SQLModel into SQLAlchemy, and you need to read both to understand it. Developers who already know SQLAlchemy well sometimes find that the saved lines are not worth the extra layer in the stack trace. Developers who are new to both get a gentler entry point, because the README's example is a working program in about twenty lines. Pick based on how much of your schema is plain columns and how much is the kind of thing that needs sa_column.

## Maintenance, licence and what an upgrade costs

The repository is not archived, and the last push was on 2026-09-01. Three releases landed within minutes of each other on 2026-08-28: 0.0.42, 0.0.41 and 0.0.40. That pattern suggests small, rapid patch releases rather than long release trains, and it means you should read the release notes at sqlmodel.tiangolo.com/release-notes/ before bumping, because the changelog is listed as a project URL in pyproject and is the only upgrade record the material points to.

Upgrade cost is mostly inherited. The dependency range SQLAlchemy >=2.0.14,<2.1.0 means a SQLAlchemy 2.1 release will require a new SQLModel version before you can move. Pydantic is pinned at >=2.11.0 with no upper bound, so a Pydantic major release is the riskier of the two to absorb. Budget for testing your model definitions, not just your queries, after either dependency moves.

The licence is MIT, declared in pyproject with license-files = ["LICENSE"]. MIT is permissive and permits commercial use and modification with the copyright notice retained. That is a statement about the licence text, not legal advice for your situation; check the LICENSE file in the repository and your own organisation's policy.

## Conclusion

Adopt SQLModel if you are building a FastAPI service and want one typed class to serve as both the table definition and the response model, and if you can live with a 0.0.x version line and hand-written migrations. Do not adopt it if you need SQLAlchemy's full ORM surface, mature async patterns, or a stable API guarantee; the pyproject classifier still says Development Status 4 - Beta. Before committing, verify three things: that your SQLAlchemy version falls inside the declared range of >=2.0.14,<2.1.0, that your Python is 3.10 or newer, and that your migration tooling can read the metadata SQLModel.metadata.create_all(engine) builds.

## FAQ

### Is SQLModel an ORM?

Yes. SQLModel is a library for interacting with SQL databases from Python using Python objects, and the README describes it as a thin layer on top of Pydantic and SQLAlchemy. The object-to-table mapping itself is SQLAlchemy's.

### What is the difference between SQLModel and SQLAlchemy?

SQLModel sits on top of SQLAlchemy and reuses it rather than replacing it, so anything SQLAlchemy does is still available underneath. The difference is that SQLModel lets one annotated class serve as both the SQL table and the Pydantic validation model, removing the need to define the two separately.

### How do you install SQLModel?

The README says to install uv first and then run uv add sqlmodel, which also pulls in SQLAlchemy, Pydantic and typing-extensions. If you prefer pip, the README says to install sqlmodel inside a virtual environment and points to its installation guide for those steps.

### How do you use SQLModel with FastAPI?

The README states that SQLModel was designed to simplify interacting with SQL databases in FastAPI applications and was created by the same author. Because a SQLModel class is also a Pydantic model, the same class you mark with table=True can be used where FastAPI expects a schema, which is the duplication the README sets out to remove.

### What is a session in SQLModel?

A Session is the object you open to talk to the database. In the README example you instantiate it with the engine, add model instances inside it, and call commit to save them, and queries are run by passing a select statement to session.exec.

### What is sa_column in SQLModel?

The README does not document sa_column. Based on the dependency on SQLAlchemy and the README's description of SQLModel as a thin layer over it, sa_column is the point where you supply a SQLAlchemy column definition directly instead of relying on the annotation, and you should confirm the exact behaviour in the SQLModel documentation before using it.

## Sources

- [fastapi/sqlmodel on GitHub](https://github.com/fastapi/sqlmodel)
- [License: MIT](https://github.com/fastapi/sqlmodel/blob/main/LICENSE)
- [Project website](https://sqlmodel.tiangolo.com/)
- [README](https://github.com/fastapi/sqlmodel/blob/main/README.md)
- [Releases](https://github.com/fastapi/sqlmodel/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/fastapi-sqlmodel
