sqlite-utils 4.0 adds schema migrations and nested transactions
sqlite-utils 4.0 ships database schema migrations, db.atomic() for nested transactions, and compound foreign key support for SQLite-backed Python projects.
TL;DR
sqlite-utils 4.0 is out, and the headline addition is a built-in migration system that tracks and applies schema changes to SQLite databases — no external migration tool required.
What happened
Simon Willison released sqlite-utils 4.0 on July 7, 2026, marking the project’s 124th release and its first major version bump since 3.0 landed in November 2020. The release ships three new features: a database migration system, nested transaction support via a new db.atomic() method, and compound foreign key support. There are also some small but breaking API changes, covered in a dedicated upgrade guide on the project’s documentation site.
The migration system is the centerpiece. It lets you define schema changes as a sequence of decorated Python functions inside a plain .py file, then apply any pending ones with a single CLI command. Under the hood, it leans on sqlite-utils’s existing table.transform() method, which handles schema operations that SQLite’s native ALTER TABLE doesn’t support — things like renaming columns or changing column types — by creating a new temporary table, copying the data over, and swapping it in.
Code example
# pip install sqlite-utils
from sqlite_utils import Migrations
migrations = Migrations("creatures")
@migrations()
def create_table(db):
db["creatures"].create(
{"id": int, "name": str, "species": str},
pk="id",
)
@migrations()
def add_weight(db):
db["creatures"].add_column("weight", float)
@migrations()
def change_column_types(db):
db["creatures"].transform(types={"species": int, "weight": str})
Save that as migrations.py, then run it against a database file:
uvx sqlite-utils migrate data.db migrations.py
Each decorated function is a migration step. sqlite-utils tracks which ones have already been applied, so re-running the command is safe — only pending migrations execute.
Why it matters
If you’ve used sqlite-utils for scripting or Datasette projects, you’ve probably hit the wall where your database schema needs to evolve and you’re left copying tables by hand or reaching for Alembic — which is a heavy dependency for a lightweight SQLite workflow. This fills that gap without pulling in SQLAlchemy or any additional infrastructure. The decorator-based API is genuinely clean: your migration history lives as readable Python functions, ordered by definition, and the CLI handles the rest.
The table.transform() trick is worth understanding. SQLite is notoriously limited with ALTER TABLE — you can add columns but you can’t change types or rename them in older SQLite versions without rebuilding the table. sqlite-utils has automated that rebuild pattern for a while now, and the new migration system makes it first-class. For developers building tools or internal apps on top of SQLite (which, honestly, covers a lot of ground — embedded apps, CLI tools, local-first web apps), this is a meaningful quality-of-life upgrade. It won’t replace Alembic for a production Postgres setup, but for the SQLite niche it serves, it’s the right-sized solution.
What to watch
- Watch whether Datasette’s own tooling adopts the migration system natively, since that’s the primary ecosystem sqlite-utils feeds.
- Check the upgrade guide before updating any existing projects — the breaking changes from 3.x are small but real.