Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

iTechGuides is reader-supported. When you buy through links on our site, we may earn an affiliate commission. As an Amazon Associate I earn from qualifying purchases. Learn more

A static checker can catch one specific and risky kind of Django migration drift without importing Django, loading settings, or running a manage.py command. The author’s write-up describes this as a way to check models against migrations on a cold CI runner or in a broken virtual environment, where Django’s own check cannot start. The technique covers one direction only: a model field that is declared in models.py but has not been migrated yet.

The problem: Django’s check needs a working Django

The standard way to find migration drift in a Django project is makemigrations --check. It compares the current model definitions with the migration history and exits with an error if the models describe changes that no migration yet records. The author of the write-up treats it as the right answer for this problem, with one practical condition: it needs the project’s dependencies installed and its settings to import cleanly.

That condition fails in exactly the places where drift checks are most useful. A fresh CI runner may not have the full dependency set cached. A virtual environment may have a package that no longer installs, or a settings module that reads an environment variable the job does not provide. In those cases the check cannot start, so it cannot report anything.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How the static approach works

The alternative in the write-up does not execute the project. It reads files as text and builds two representations that can be compared.

Step 1: Read the migration files

The checker reads each app’s migration modules and walks their operations in order. Operations that add a field contribute that field to a running set for the relevant model.

Step 2: Replay the operations into a field set

Replaying the operations produces the field set the migrations would leave behind. This is a reconstruction from declarations, not the database schema and not the state Django builds at runtime.

Step 3: Parse the model declarations

The checker reads each model declared in models.py and collects the field names it declares. Because this is source inspection, no model class is instantiated and no app registry is populated.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Step 4: Compare the two sets

Any field present in the model declarations but absent from the replayed set is reported as declared but never migrated. According to the author, replaying the migration files and diffing against models.py was enough to catch this direction of drift without requiring the normal environment.

What it detects, and what it does not

The claim is deliberately narrow. The write-up describes catching the dangerous direction of drift: code that expects a column the migrations will never create. That is the failure that breaks a deploy at query time, which is why it was chosen as the first target.

The write-up does not establish that the checker detects other kinds of mismatch. Nothing in the available description shows that it validates every migration operation, handles every form of field removal or rename, or confirms that a migration produces the same schema Django would build. Treat it as a guard for one failure mode, not as a replacement for Django’s migration machinery or for running migrations against a real database.

Where it fits

  • Cold CI runners where installing the full project dependency set is slow or unavailable and the job only needs to fail fast on a missing migration.
  • Broken virtual environments where manage.py cannot start, so Django’s own check produces no output.
  • Pre-check stages that run before the heavier test or migration jobs, so an unmigrated field is caught early with a clear message.

When the full environment works, makemigrations --check remains the more complete tool, because it uses Django’s own model state rather than a reconstruction.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Comparing the two approaches

Aspect makemigrations --check Static replay and diff
Environment prerequisites Installed dependencies and settings that import cleanly Source files only; no Django import, no settings load
Drift direction covered Django’s own comparison of model state against migrations, as Django defines it Fields declared in models but not migrated, as described by the author
Source of truth for model state Django’s runtime model state Reconstruction from migration operations and parsed declarations
Framework-aware validation Provided by Django itself Not established in the write-up
Reported speed Not stated in the write-up Author reports about 20 ms to parse model graphs for Zulip, Saleor, Wagtail, django CMS, and Mezzanine together, on a laptop

The timing figure comes from the author alone. The write-up does not describe the hardware, the measurement method, or the version of each project used, and no independent benchmark has been published for it. The article’s publication date is also not shown in the available text, so the figure should be read as a report about that version of the checker, not a current measurement.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Applying the idea in your own project

If you want the same guard, the logic is simple enough to reproduce: collect field additions from migrations, replay them per model, parse field declarations from models.py, and fail when a declared field has no matching addition. Keep the scope the same as the author’s. A failing result means a field is missing from the migrations. A passing result does not prove the migrations match the models in every other respect.

The write-up’s own repository, installation steps, supported versions, and test methodology are not described in the material available to this article, so this article does not specify them.

The original write-up is by FROWNINGdev on DEV Community: I built a Django linter that never imports Django — DEV Community.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Bottom Line

Use the static check as an early, environment-independent guard for unmigrated fields, and keep makemigrations --check as the authoritative test wherever Django can run.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.