Every time I land on an unfamiliar Django codebase I hit the same wall: to see
how the models actually relate, the project has to run. Settings, a database, a
working venv. On a cold clone that’s an afternoon before I’ve learned anything.
So I wrote a parser that reads models.py directly with the ast module. No
database, no runserver, no venv, no Django import:
uvx django-orm-lens scan
That prints every app, model, field and relation in the project. From the same
parser I built three surfaces:
- A VS Code sidebar with a live ER diagram (Mermaid, DBML, D2 or PlantUML)
- A CLI for CI: static N+1 detection, 16 migration-risk rules, SARIF output for
code scanning, pre-commit hooks, a GitHub Action - An MCP server, so a coding agent answers schema questions from the real models
instead of hallucinating field names
The parsing is the interesting part — it has to survive every way people write a
ForeignKey. The test suite keeps golden snapshots of models vendored from Zulip,
Saleor, Wagtail, django CMS and Mezzanine: 59 models parsed in about 21 ms.
Since I first posted this, three things landed that are worth naming separately:
- blast-radius joins the migration-risk rules to a workspace-wide reference
scan. For every destructive operation it reports what still reads the thing
being dropped, grouped by Django layer, each finding tagged certain / likely /
possibly. As a GitHub Action it posts one PR comment and updates that same
comment on later pushes. - drift replays each app’s migrations into the field set they imply and diffs
that against models.py. It is makemigrations --check without a settings
module or an app registry, so it runs on a cold clone. - The N+1 detector now follows a queryset out of the function it was built in,
sofor post in recent():is analysed. A select_related added either inside
the helper or by the caller silences it.
MIT, Django 4.2-5.2, Python 3.10-3.12. Everything above is in the free package —
there is no paid tier.
Two things I’d genuinely like input on:
- Third-party fields. django-mptt and django-taggit are invisible to the parser
right now because it matches a whitelist of Django’s own field types.
TreeForeignKey and TaggableManager are the two I keep hitting — if you
maintain or use a package that declares its own field types, I’d rather match
your actual API than guess at it. - The migration-risk rules. I have 16, mostly around table locks and
irreversible data migrations. If you’ve been burned by a migration in
production, I’d like to know how — that’s the best source of new rules.
