A conservative command-line codemod that upgrades legacy SQLAlchemy 1.x code to modern 2.0 style — it rewrites the patterns that are provably safe and flags the rest with actionable guidance, so you never end up with silently-broken code.
By default it prints a unified diff (a dry run) and changes nothing. Every finding is tagged with a stable rule id and a one-line explanation, and unrewritable patterns are reported instead of guessed at.
Given this legacy module:
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
def get_user(session, user_id):
return session.query(User).get(user_id)
def all_users(session):
return session.query(User).all()sqla-1to2-codemod app.py --write produces:
from sqlalchemy.orm import declarative_base
from sqlalchemy import select
Base = declarative_base()
def get_user(session, user_id):
return session.get(User, user_id)
def all_users(session):
return session.execute(select(User)).scalars().all()Rewrites preserve your formatting and comments — the tool is built on LibCST, a concrete syntax tree, not a regex pass.
The tool is distributed from GitHub (it is not on PyPI):
pip install "git+https://github.com/async-workflows/sqla-1to2-codemod.git"It requires Python 3.10+ and installs LibCST as its only runtime dependency.
Scan a path (dry run — prints a diff of proposed changes and a grouped summary of flags, exit code 0):
sqla-1to2-codemod path/to/project/| Flag | Effect |
|---|---|
| (none) | Dry run: print a unified diff plus a grouped flag summary, exit 0. |
--write |
Apply the safe rewrites in place. |
--check |
Exit non-zero if any legacy pattern is found (for CI gates). |
--rules SA001,SA003 |
Run only the listed rules. |
--json |
Emit a machine-readable report: a list of {file, line, rule_id, summary, fixable}. |
--explain SA004 |
Print a rule's full explanation and documentation link, then exit. |
--list-rules |
List every available rule and exit. |
--version |
Print the version and exit. |
You can pass multiple files or directories; directories are searched recursively for
.py files.
| Rule | Pattern | Action |
|---|---|---|
SA001 |
from sqlalchemy.ext.declarative import declarative_base |
Auto-fix → from sqlalchemy.orm import declarative_base |
SA002 |
from sqlalchemy.ext.declarative import declared_attr |
Auto-fix → from sqlalchemy.orm import declared_attr |
SA003 |
session.query(Model).get(pk) |
Auto-fix → session.get(Model, pk); Model.query.get(pk) is flagged |
SA004 |
session.query(...) |
Auto-fix the simple .all() shape → session.execute(select(...)).scalars().all(); richer chains are flagged |
SA005 |
Query.filter(...) / filter_by(...) |
Flag — migrate to select().where(...) |
SA006 |
engine.execute(...) / conn.execute("raw sql") |
Flag — use a connection + text() in an explicit transaction |
SA007 |
x = Column(...) in a model |
Flag — adopt x: Mapped[...] = mapped_column(...) |
Run sqla-1to2-codemod --explain SA00X for the full rationale and a link to the
relevant migration guide.
Use --check to fail a build when legacy patterns remain. It exits non-zero as soon
as any rule matches, so it doubles as a ratchet once a module is migrated:
# .github/workflows/ci.yml
- name: Check for legacy SQLAlchemy 1.x patterns
run: |
pip install "git+https://github.com/async-workflows/sqla-1to2-codemod.git"
sqla-1to2-codemod src/ --checkPrefer machine-readable output? sqla-1to2-codemod src/ --json emits one JSON object
per finding for your own dashboards or review bots.
- Rewrites are deliberately narrow. Only shapes that are provably behaviour-
preserving are changed (import relocations,
Query.get(), and the single-entitysession.query(Model).all()). Everything else is flagged so a human keeps control. SA004only rewrites the trivial.all()case. Any filters, ordering, joins, column selections, or terminal methods such as.first()/.one()are reported, not auto-migrated.SA007never rewrites. TurningColumn(Integer)intoMapped[int]would flip a column from nullable to NOT NULL, since 2.0 infers nullability from the annotation, so the codemod recommends the change but leaves the exact annotation to you.- Heuristics can over-flag.
SA006keys off receiver names (engine,conn,connection) and string-literal arguments, so a non-SQLAlchemycursor.execute("...")may be flagged. Review flags before acting on them. - Run it in dry-run mode first, review the diff, then re-run with
--write. Rewrites are idempotent — running twice makes no further changes.
Deeper background on the migration these rules automate:
- Migrating legacy 1.4 code to 2.0 syntax — the overall upgrade path.
- The legacy 1.4-to-2.0 codemod checklist — a step-by-step companion to this tool.
- Replacing
Query.filterwithselect().where— the reasoning behindSA004andSA005. - Fixing
RemovedIn20Warningdeprecation warnings — context forSA001,SA002, andSA006. - A step-by-step guide to SQLAlchemy 2.0 type annotations — the typed
Mapped[...]style behindSA007.
MIT © 2026 async-workflows