django-migration-linter

Detects backward-incompatible Django migrations before they break deploys, CI, or zero-downtime rollbacks.

Tool
PyPI
v6.0.0
615stars
Apache License 2.0

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum.How we score it →
47/100Fair
Development Activity0
Maintenance32
Community68
Maturity60
Momentum28

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation.How we score it →
78/100Good
Architecture78
Code Quality82
Innovation68
Learning Curve85

django-migration-linter is a Django app and management command that inspects your project’s migrations and flags operations that are not backward compatible with the currently running code. It works by generating the raw SQL each migration would execute (via sqlmigrate) and pattern-matching that SQL against a set of known-unsafe operations: dropping a NOT NULL column without a default, dropping columns or tables, renaming columns or tables, altering column types, and adding unique constraints outside of a table creation.

The tool is built specifically for teams running rolling or zero-downtime deployments, where an old application process can still be serving requests against a database that has already had a new migration applied. A migration that drops a column the old code still reads, or adds a NOT NULL column with no default, will crash those in-flight requests — this linter catches that class of bug before it reaches production, either as a CI gate (lintmigrations) or interactively at generation time (makemigrations --lint).

Beyond raw SQL analysis, it also inspects Python data migrations (RunPython/RunSQL operations) for common mistakes: missing reverse migrations, incorrect argument naming, and importing Django models directly instead of via apps.get_model() (which breaks historical migrations). Results are cached by migration file hash so repeated CI runs only re-analyze what changed. Individual checks can be selectively ignored via --exclude-migration-tests or promoted to hard errors via --warnings-as-errors, and the linter ships analysers for SQLite, MySQL, and PostgreSQL.

What You Get

  • A lintmigrations management command that lints all migrations, one app’s migrations, or a single named migration
  • A makemigrations --lint integration that checks and can auto-delete a newly generated migration on the spot
  • SQL-level detection of NOT NULL columns without defaults, dropped columns/tables, renamed columns/tables, altered columns, and added unique constraints
  • Data-migration linting for RunPython/RunSQL operations, including reversibility checks and model-import correctness
  • Per-migration result caching (keyed by file hash) so CI re-runs only analyze changed migrations
  • Configurable analysers for SQLite, MySQL, and PostgreSQL, selected automatically from Django’s DATABASES setting
  • Fine-grained suppression via --exclude-migration-tests and escalation via --warnings-as-errors, plus --git-commit-id to lint only migrations added since a given commit

Common Use Cases

  • Gating CI/CD pipelines so a pull request cannot merge a migration that would break a zero-downtime deploy
  • Running as a pre-commit or local git hook to catch unsafe migrations before they’re even pushed
  • Auditing an existing Django project’s full migration history for latent backward-incompatible operations
  • Enforcing safe migration patterns (e.g. requiring a default value on new NOT NULL columns) across a multi-team codebase
  • Blocking or interactively confirming risky migrations at generation time via makemigrations --lint

Under The Hood

Architecture The core MigrationLinter class (src/django_migration_linter/migration_linter.py) drives a straightforward pipeline: it loads Django’s MigrationLoader to enumerate on-disk migrations, optionally filters them by app, name pattern, git diff since a commit, or applied/unapplied state, then for each migration calls Django’s sqlmigrate management command to materialize the actual SQL, hashes the migration file for cache lookup, and hands the resulting SQL statements to a pluggable sql_analyser for pattern matching. RunPython/RunSQL operations get a second, separate analysis pass (analyse_data_migration) that inspects the migration’s Python source via inspect.getsource and regex rather than SQL. A Cache class persists per-migration-hash verdicts between runs so unchanged migrations are skipped on subsequent CI executions. The design cleanly separates “what SQL will run” (delegated to Django itself via sqlmigrate) from “is this SQL safe” (the linter’s own concern), which keeps the tool decoupled from migration-generation internals.

Tech Stack Pure Python, distributed as a standard Django app (INSTALLED_APPS entry) with two custom management commands (lintmigrations, an overridden makemigrations). Runtime dependencies are minimal — django>=2.2, appdirs for cache-directory resolution, and toml for reading configuration from pyproject.toml/setup.cfg/tox.ini. The test matrix (via tox) spans Python 3.9 through 3.14 against Django versions 3.2 through 6.0, with a dedicated GitHub Actions workflow driving that matrix plus a pre-commit-based lint environment. Optional test extras pull in mysqlclient and psycopg2 so the SQL analysers can be exercised against real MySQL and PostgreSQL backends, not just SQLite.

Code Quality The project ships a py.typed marker and type-annotated source throughout, uses from __future__ import annotations consistently, and has separate tests/unit and tests/functional suites plus a tests/test_project fixture Django project used to exercise real migrations end-to-end. CI runs the full tox matrix across Python and Django version combinations on every push, and a dedicated lint tox environment runs pre-commit checks. Error handling around SQL and git-diff subprocess calls is explicit, with dedicated exceptions raised on failure rather than silently swallowed. There’s an explicit # Fixme comment acknowledging one area (error/warning/ignored/ok branching) the maintainers consider due for a more generic rewrite — a level of self-documented technical debt that’s easy to verify against the current code.

What Makes It Unique Rather than statically parsing migration operation objects, it delegates to Django’s own sqlmigrate command to get the exact SQL that will run against the configured database backend, then analyses that real SQL — meaning its checks stay accurate across Django ORM changes and correctly reflect backend-specific SQL dialects (SQLite/MySQL/PostgreSQL each get their own analyser). Combining this SQL-level analysis with a separate Python-source inspection pass for data migrations (catching non-reversible RunPython operations and incorrect apps.get_model() usage) is a level of migration-safety coverage most linters in this space don’t attempt.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers

Search