# SQLAlchemy's Core and ORM are two libraries, not one

> The most misread feature of the Python SQL toolkit is that its ORM is optional, built on a Core that is a full abstraction layer, and says so in the README.

**sqlalchemy/sqlalchemy** — The Database Toolkit for Python

- Repository: https://github.com/sqlalchemy/sqlalchemy
- Website: https://www.sqlalchemy.org
- Stars: 12,262 · Forks: 1,830
- Language: Python
- License: MIT
- Published: 2026-10-07 · Updated: 2026-10-07 · Language: en
- Canonical page: https://hysenlabs.com/projects/sqlalchemy-sqlalchemy

## The philosophy list is the actual documentation

Most Python database libraries present a feature list. SQLAlchemy's README presents a philosophy list, and it is the most useful thing in the file. Seven positions, stated as rules rather than benefits.

The first says SQL databases behave less and less like object collections as size and performance start to matter, while object collections behave less and less like tables and rows as abstraction starts to matter, and SQLAlchemy aims to accommodate both. That is a single sentence containing the entire design tension the library exists to manage.

The next two are the ones that change how you write code. An ORM does not need to hide the R: a relational database provides set-based functionality that should be fully exposed. And the developer makes all decisions about the design, structure and naming of both the object model and the relational schema, because SQLAlchemy only automates the execution of those decisions.

Two more deserve quoting mentally. There is no such thing as the ORM generated a bad query, because you retain control over joins, subqueries, correlation and which columns are requested. And do not use an ORM if the problem does not need one. A library that tells you when to leave it alone is rare enough to be worth taking seriously.

## Core is a database abstraction layer in its own right

The feature list describes SQLAlchemy Core as separate from the ORM and as a full database abstraction layer in its own right. Its contents are named: an extensible Python-based SQL expression language, schema metadata, connection pooling, type coercion and custom types.

Then the philosophy list sharpens it. The Core offers the full SQL expression language for Pythonic construction of SQL constructs that render directly to SQL strings for a target database, returning result sets that are essentially enhanced DBAPI cursors.

That is the sentence that tells you when to use Core instead of the ORM. If you want statements, you want Core. A reporting query, a bulk update, a migration script, a place where mapping your rows onto domain objects adds nothing: all of those are Core, and using the ORM there is the classic mistake people make when they arrive from a Django or Hibernate background.

There is also a feature here that is easy to overlook. Database introspection and generation: a schema can be reflected in one step into Python structures representing database metadata, and those same structures can generate CREATE statements back out, all within Core, independent of the ORM. Reflection and metadata are not ORM features in this library, which matters when your schema is owned by a migration tool rather than by your models.

## Identity map, unit of work, data mapper

The ORM is described as being built on three named patterns, and it is worth being precise about what each one buys.

The identity map means one Python object per row identity within a session, so the same row fetched twice is one object, not two. The unit of work means changes to those objects are tracked and synchronized with the current transaction automatically, and the README is explicit about the consequence: nothing goes to permanent storage until `commit()` is called. The data mapper is the piece that separates rows from objects at all, which is the point the README makes when it says an ORM does not need to hide the R.

On top of those sit two systems that do most of the day-to-day work.

Eager loading is described as a comprehensive and flexible system for related collections and objects, with three named strategies: loaded on individual access, all at once using joins, or by query per collection across the full result set. Collections are cached within a session, so the strategy you choose at a call site determines how many round trips you pay for later.

The query system is relationally oriented and exposes the full range of SQL explicitly, including joins, subqueries and correlation, in terms of the object model. The README says dropping into literal SQL is possible at any time and that it is virtually never needed.

## Keys, transactions and the injection argument

Four philosophy entries deserve their own treatment because they are the ones with security and correctness weight.

Composite keys: all primary and foreign key constraints are assumed to be composite and natural, and SQLAlchemy never assumes or hardcodes the surrogate integer primary key model, while noting that surrogate integer keys are of course still the norm in practice. So a library with a reputation for convenience still does not force one key style on you.

Transactions: the ORM's rule is that nothing is permanent until `commit()`, and the project encourages applications to define a consistent way to delimit the start and end of a series of operations. The README does not name a context manager or a decorator here, which is a reminder that this is a principle rather than an API.

Bound parameters: never render a literal value in a SQL statement. The stated reason is twofold. Query optimizers can cache query plans effectively, and SQL injection attacks become a non-issue. That second claim is about the library's own construction, and it only holds while you use the parameter path, which is why it is phrased as a rule rather than a feature.

## What installing it actually requires

The README punts installation to the documentation, which is reasonable for a library with a dialect matrix, and the packaging files say more than a setup command would.

`pyproject.toml` sets the Python floor and keeps every driver optional:

```toml
requires-python = ">=3.11"
dependencies = [
    "typing-extensions >= 4.6.0",
]
asyncio = ["greenlet>=1"]
```

The classifiers cover CPython and PyPy across Python 3.11 to 3.15. Everything database-specific is an extra: `asyncio` pulls in greenlet, `mssql` pulls pyodbc, `mysql` pulls mysqlclient, and the pattern continues per driver. So a project that talks to one database installs one extra, not a driver for every server in existence.

The Cython part is the detail that surprises people. The build requires `setuptools>=77.0.3` and `cython>=3.3` on CPython, and `setup.py` compiles eight extension modules: `engine._processors_cy`, `engine._row_cy`, `engine._result_cy`, `engine._util_cy`, `sql._cache_key_cy`, `sql._util_cy`, `util._collections_cy` and `util._immutabledict_cy`.

Two environment variables control that. `DISABLE_SQLALCHEMY_CEXT` and `REQUIRE_SQLALCHEMY_CEXT`, and setting both raises a RuntimeError at setup time. So the extension modules are optional, mandatory or default depending on your build, and the choice is explicit rather than implicit. From 2.0.45 onward, no binary wheels are published for Python 3.7 at all, because PyPI began rejecting wheel filenames that do not begin with the normalised project name.

## Release cadence and the examples directory

Three releases in the recent window tell you what maintenance looks like here. `rel_2_0_54` on 2026-09-15, `rel_2_0_53` on 2026-09-14, and `rel_2_1_0rc2` on 2026-09-08, with the last push to main on 2026-09-19. So there is a maintained 2.0 line shipping bug fixes daily and a 2.1 line in release candidate, which is the shape of a project with a large installed base rather than a small one.

The release notes are also unusually specific. The 2.0.53 entry describes a bug where an expression embedding a `select()`, such as a correlated `exists()`, failed to populate an attribute on the second and subsequent executions of an otherwise identical statement when the attribute was loaded by `selectinload`, `lazyload` or `immediateload`. That is a real ORM problem reported with enough detail that you can tell whether you have it.

The `examples/` directory is the other place to look, and its names read like an argument about what ORM problems are hard. Modelling shapes come first: `inheritance/`, `composite/`, `versioned_rows/`, `versioned_history/`, `association/`, `generic_associations/`. Then the tree structures that produce recursive queries: `adjacency_list/`, `nested_sets/`, `materialized_paths/`, `graphs/`. Then the operational concerns: `performance/`, `sharding/`, `dogpile_caching/`, `asyncio/`.

If you are evaluating an ORM, read `examples/versioned_rows/` and `examples/nested_sets/`. If a library handles those cleanly, the impedance mismatch argument gets a lot quieter.

## Conclusion

SQLAlchemy is at its best when a team owns its schema and wants SQL to stay visible while the object handling gets out of the way, which is exactly what the Core and ORM split is for. It is the wrong tool when you want to delete your schema and let the ORM own it, since SQLAlchemy never generates your design for you, or when you need a small dependency-free query layer, since the import pulls in a large package with Cython extensions. Verify three things before adopting it. Check the Python floor in `pyproject.toml` against your runtime, because the declared requirement is 3.11 and above. Decide whether the C extensions are enabled in your build, since `DISABLE_SQLALCHEMY_CEXT` and `REQUIRE_SQLALCHEMY_CEXT` change the character of the install. Then read the philosophy list in the README before writing code, because the first item there says not to use an ORM if the problem does not need one.

## FAQ

### What is SQLAlchemy used for?

It is the Python SQL toolkit and object relational mapper, giving application developers SQL's full power and flexibility in a Pythonic domain language. It ships two layers: an ORM built on the identity map, unit of work and data mapper patterns, and a separate Core that is a full database abstraction layer with an expression language, pooling and type coercion.

### Is SQLAlchemy free to use?

Yes. The README states SQLAlchemy is distributed under the MIT license, and `pyproject.toml` declares `license = "MIT"` with LICENSE and AUTHORS as license files. That covers commercial use as well as open source projects.

### Why do we need SQLAlchemy?

The README's own answer is that it gives developers the full power and flexibility of SQL through a suite of enterprise-level persistence patterns, without hiding the relational model. Its philosophy list adds that a database provides set-based functionality that should be exposed, and that the developer keeps control over joins, subqueries and which columns are requested.

### Is SQLAlchemy the best ORM for Python?

The README does not make that claim, and its philosophy list says not to use an ORM if the problem does not need one. It argues instead that you keep full control over query structure and that the ORM is one component, separate from the Core abstraction layer, which you can use on its own for statements and reporting queries.

## Sources

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

---

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