Filter concepts by levelShowing all levels.

Django · Migrations

Migrations

makemigrations/migrate, the dependency graph, forward/reverse, data migrations, fake, and squashing

  • makemigrations diffs; migrate appliesthe first never touches the database, the second always does
  • One graph across every appa migration can depend on a completely different app's migration
  • RunPython/RunSQL have no auto-reverseomit reverse_code/reverse_sql and rollback hits IrreversibleError
  • apps.get_model(), never a real importa historical model, frozen at this migration's point in time

1.makemigrations, migrate, and the dependency graph

makemigrations compares current models against migration history and WRITES a new migration file describing the difference — it never touches the database. migrate READS migration files and actually APPLIES (or unapplies) them against the real database. Each migration file lists dependencies (which other migrations must run first) and operations (what to actually do) — Django builds an in-memory graph from every app's dependencies to compute one consistent apply order. Two developers each adding a migration on the same base, in parallel branches, produces a CONFLICT — makemigrations --merge resolves it by creating one migration depending on both.

Key features

  • makemigrations never touches the database — it only reads models.py and writes a migration file; migrate is the step that actually executes SQL
  • A migration file has two key parts: dependencies (a list of (app, migration_name) tuples that must apply first) and operations (the actual CreateModel/AddField/etc. instructions)
  • Django builds one in-memory graph from every app's migrations combined — a migration can depend on a migration from a COMPLETELY DIFFERENT app, which is how cross-app ForeignKeys stay correctly ordered
  • A migration conflict happens when two migrations in the same app both declare the SAME migration as their dependency (e.g. two developers both branched off 0002 and each created their own 0003) — Django refuses to proceed until it is resolved
  • makemigrations --merge creates a new migration whose dependencies list BOTH conflicting migrations, linearizing the history into one valid graph again

makemigrations vs migrate

makemigrations vs migrate
CommandReadsWrites
makemigrationscurrent models.py files + existing migration historya new migration .py file
migratemigration files + the django_migrations tracking tablethe actual database schema
makemigrations writes a file; migrate applies it

models.py

current model definitions

makemigrations

diffs against migration history — no DB touched

0003_add_rating.py

dependencies + operations

migrate

reads the file, applies it

database schema

  • models.py — current model definitions
    • leads to makemigrations
  • makemigrations — diffs against migration history — no DB touched
    • leads to 0003_add_rating.py
  • 0003_add_rating.py — dependencies + operations
    • leads to migrate
  • migrate — reads the file, applies it
    • leads to database schema
  • database schema

Command

python
class Migration(migrations.Migration):
    dependencies = [("books", "0002_previous_migration")]
    operations = [migrations.AddField("Book", "rating", models.IntegerField(default=0))]

makemigrations writes a migration file from a models.py diff (no database touched); migrate reads migration files and actually applies them. A migration's dependencies (which can cross app boundaries) plus every app's combined graph determine one consistent apply order. A conflict (two migrations claiming the same dependency) is resolved with makemigrations --merge, not by deleting a file.

2.Forward and reverse migrations

migrate app_name applies every migration up to the latest (forward). migrate app_name 0002 rolls back to (and including) that specific migration — Django figures out which already-applied migrations need to be UNDONE and runs each one's reverse operation. migrate app_name zero rolls back every migration for that app entirely. Not every operation can be reversed — RunSQL with no reverse_sql, or a RunPython with no reverse function, raises IrreversibleError the moment a rollback tries to pass through it.

Key features

  • migrate app_name (no target) applies every migration forward, up to the latest
  • migrate app_name <migration_name> rolls back to (and including) that migration — every migration applied AFTER it gets its reverse operation run, in reverse order
  • migrate app_name zero unapplies every migration for that app, back to nothing
  • Every built-in schema operation (AddField, CreateModel, AlterField, etc.) has an automatic reverse Django computes for you — no extra code needed
  • RunPython/RunSQL have NO automatic reverse — omitting reverse_code/reverse_sql makes that specific operation (and therefore any rollback that reaches it) irreversible, raising IrreversibleError

Forward vs reverse migration commands

Forward vs reverse migration commands
CommandEffect
migrate app_nameapply every unapplied migration, forward, up to the latest
migrate app_name 0002roll back to (including) 0002 — later migrations get reversed
migrate app_name zerounapply every migration for this app entirely
Forward and reverse migrate, and where an irreversible operation blocks
migrate→ 0002migrate→ 0003migrate(forward)migrate 0002 (reverse)— IrreversibleError

zero

start

0002

0003 (RunSQL, no reverse_sql)

latest

end

  • zero (start)
    • → 0002 when migrate → 0002
  • 0002
    • → 0003 (RunSQL, no reverse_sql) when migrate → 0003
  • 0003 (RunSQL, no reverse_sql)
    • → latest when migrate (forward)
    • → 0002 when migrate 0002 (reverse) — IrreversibleError
  • latest (end)

Command

python
migrations.RunSQL(
    sql="ALTER TABLE books ADD COLUMN rating integer",
    reverse_sql="ALTER TABLE books DROP COLUMN rating",   # required for this migration to be reversible
)

migrate app_name applies forward; migrate app_name <target> (or zero) rolls back, by running each already-applied migration's REVERSE operation. Built-in schema operations reverse automatically; RunPython/RunSQL need an explicit reverse (reverse_code/reverse_sql) or they become a one-way door that blocks any rollback trying to pass through them.

3.RunPython/RunSQL, fake migrations, and squashing

A SCHEMA migration changes table structure (AddField, CreateModel); a DATA migration changes the actual row VALUES (via RunPython or RunSQL), often to backfill a new column. RunPython MUST get its models via apps.get_model("app", "Model") — a HISTORICAL, frozen-in-time model matching that exact point in migration history — never the real, current models.py import, which can drift out of sync with what the migration was actually written against. migrate --fake marks a migration as applied WITHOUT running it (for a database that already matches); squashmigrations collapses many migration files into fewer, for a cleaner history.

Key features

  • A data migration (RunPython/RunSQL) changes ROW VALUES; a schema migration (AddField, CreateModel, etc.) changes table STRUCTURE — the two are often paired (add a column, then backfill it)
  • RunPython functions receive (apps, schema_editor) — apps.get_model("app_label", "ModelName") returns the HISTORICAL model as it existed at THIS point in migration history, not the current models.py
  • Directly importing a real model (from myapp.models import Author) inside a RunPython function is a documented anti-pattern — it can break when the migration is re-run later, after the real model has changed further
  • --fake marks a migration as applied in django_migrations WITHOUT running its operations — used when the database already matches (e.g. adopting migrations on a legacy database); --fake-initial specifically auto-detects and fakes an initial migration if its tables already exist
  • squashmigrations app_name <target> collapses a range of migrations into one new file (optimizing/removing redundant operations along the way) — old files are kept temporarily (for machines mid-history) and removed later once every deployment has moved past them

Data vs schema migrations, and the two special-case commands

Data vs schema migrations, and the two special-case commands
ConceptWhat it does
Schema migrationchanges table structure (AddField, CreateModel, ...)
Data migration (RunPython/RunSQL)changes actual row values — often backfilling a new column
migrate --fakemarks a migration applied WITHOUT running it — the database must already match
squashmigrationscollapses many migration files into fewer, for a cleaner history
apps.get_model() vs a direct import, inside RunPython

apps.get_model("app", "Model")

  • A HISTORICAL model, frozen at this migration's point
  • Safe to re-run months later
  • The documented, correct way

from app.models import Model

  • The REAL, current model
  • Drifts out of sync as models.py evolves
  • A documented anti-pattern
  • apps.get_model("app", "Model")
    • A HISTORICAL model, frozen at this migration's point
    • Safe to re-run months later
    • The documented, correct way
  • from app.models import Model
    • The REAL, current model
    • Drifts out of sync as models.py evolves
    • A documented anti-pattern

Command

bash
python manage.py migrate --fake-initial   # adopting migrations on an existing database
python manage.py squashmigrations books 0004

A data migration (RunPython/RunSQL) changes row values, distinct from a schema migration changing structure. RunPython must use apps.get_model() for a HISTORICAL model — never import the real, current model, which can drift and break a migration re-run later. --fake marks a migration applied without running it (only when the database genuinely already matches); squashmigrations collapses history into fewer files for the same net effect.

Remember

  • makemigrations diffs models.py into a file (no DB touched); migrate applies migration files to the real database.
  • A migration conflict (two migrations on the same dependency) is resolved with makemigrations --merge, not by deleting a file.
  • Built-in schema operations reverse automatically; RunPython/RunSQL need an explicit reverse or they block any rollback passing through them.
  • Inside RunPython, always use apps.get_model() for a historical model — never import the real, current model, which can drift and break a later re-run.

Pro tip

  • --fake only belongs where the database genuinely already matches the migration's effect — using it on a real, unapplied change just makes Django's history lie about the database's state.
  • squashmigrations collapses many files into fewer for the same net effect — old files stay temporarily so machines mid-history can still catch up.

Next up

  • Safe Database Migrations
  • Django Managers
  • Custom QuerySets

Sources

  • Migrations — docs.djangoproject.com/en/5.2/topics/migrations/
  • Every claim checked against Django 5.2 LTS docs · checked 2026-08-24

Spotted a mistake on this page? Tell us.