Migrations

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Django 6.1 (6.1.1, released 2026-09-02; 6.1.2 is scheduled for 2026-10-06) on Python 3.12 to 3.14, with the 5.2 LTS differences noted where they matter, and was checked on 2026-10-03. Every example ran on Django 6.1.1 and Python 3.13 with SQLite, in a throw-away project with a small catalog app (the Bookshelf catalogue that grows across this section), so the SQL shown is SQLite’s. Where PostgreSQL behaves differently the text says so, and the PostgreSQL-only operations (AddIndexConcurrently and friends) are described but were not run because no PostgreSQL server was available.

Models describe what the database should look like now. Migrations are how Django gets an existing database from one shape to the next without losing its data: a version-controlled, ordered history of schema changes (and, when you need it, data changes) that every developer, test run and production server replays in the same way. This page covers the commands, the anatomy of a migration file, the graph that orders migrations across apps, merging, reversibility, data migrations, hand-written SQL, separating the database from the model state, squashing, non-atomic and zero-downtime deployments, custom operations, serialization, multiple databases and how to test it all. The field definitions are in Models & Fields, constraints and indexes in Constraints, Indexes & Composite Keys, and the PostgreSQL operations in PostgreSQL & Database Backends.

What migrations are and where they run

A migration is a normal Python file, in the migrations package of an app, that declares a Migration class with a list of dependencies and a list of operations. Django does three different jobs with that history:

  • Compute the state. Running the operations in memory, in order, reconstructs what every model looked like at a given point of history (the project state). This state is also what makemigrations compares with your current models.py to find out what changed, which is why the operations are declarative and why you must tell Django about every schema change (see SeparateDatabaseAndState).

  • Generate SQL. Each operation asks the database’s schema editor to turn "create a model" or "add a field" into SQL for the backend in use.

  • Record what has been applied. Applied migrations are stored in the django_migrations table, one row per (app, name).

Migrations are not part of the request/response cycle. They are a build and deploy step: you create them on your machine with makemigrations, commit them with the model change, and run migrate on every other environment before (or while) the new code starts serving requests. The development server only checks and prints a notice about unapplied migrations (runserver calls check_migrations()).

flowchart LR A["Edit models.py"] --> B["makemigrations
autodetector compares models
with the replayed migration state"] B --> C["Review the migration file
sqlmigrate, edit if needed"] C --> D["migrate on the dev database
(and run the tests)"] D --> E["Commit models.py and the
migration together"] E --> F["CI: makemigrations --check
and the test suite"] F --> G["Deploy: migrate
then switch traffic"]

Source: Migrations.

The commands

Four commands cover the daily workflow, and two more help you keep the history small.

Table 1. The migration commands
Command What it does Options worth knowing

makemigrations [app_label …​]

Creates new migrations from the differences between your models and the migration history.

--name/-n, --empty, --dry-run, --check, --merge, --update, --noinput, --scriptable, --no-header

migrate [app_label] [migration_name]

Applies and unapplies migrations until the database matches the target.

--plan, --fake, --fake-initial, --run-syncdb, --check, --prune, --database, --noinput

sqlmigrate app_label migration_name

Prints the SQL a migration would run, without running it.

--backwards, --database

showmigrations [app_label …​]

Lists migrations and whether they are applied.

--list/-l (default), --plan/-p, --database; -v 2 adds the applied dates or the dependencies

squashmigrations app_label [start] name

Collapses a range of migrations into one that replaces them.

--squashed-name, --no-optimize, --noinput

optimizemigration app_label name

Rewrites one migration with its operations optimised (for example AddField followed by AlterField becomes one AddField).

--check

Source: django-admin: makemigrations and the entries for migrate, sqlmigrate, showmigrations, squashmigrations and optimizemigration on the same page.

A first migration, end to end

The Bookshelf catalogue starts with two models.

# catalog/models.py
from django.db import models


class Author(models.Model):
    name = models.CharField(max_length=100)

    def __str__(self):
        return self.name


class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
python manage.py makemigrations catalog
python manage.py sqlmigrate catalog 0001
python manage.py showmigrations catalog
python manage.py migrate catalog
python manage.py makemigrations --check
Migrations for 'catalog':
  catalog/migrations/0001_initial.py
    + Create model Author
    + Create model Book
BEGIN;
--
-- Create model Author
--
CREATE TABLE "catalog_author" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(100) NOT NULL);
--
-- Create model Book
--
CREATE TABLE "catalog_book" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "title" varchar(200) NOT NULL, "author_id" bigint NOT NULL REFERENCES "catalog_author" ("id") DEFERRABLE INITIALLY DEFERRED);
CREATE INDEX "catalog_book_author_id_b0849980" ON "catalog_book" ("author_id");
COMMIT;
catalog
 [ ] 0001_initial
Operations to perform:
  Apply all migrations: catalog
Running migrations:
  Applying catalog.0001_initial... OK
No changes detected

The generated file is plain Python and is meant to be read and committed (the id column is a BigAutoField because DEFAULT_AUTO_FIELD defaults to BigAutoField since Django 6.0):

# catalog/migrations/0001_initial.py
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

    initial = True

    dependencies = []

    operations = [
        migrations.CreateModel(
            name="Author",
            fields=[
                ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
                ("name", models.CharField(max_length=100)),
            ],
        ),
        migrations.CreateModel(
            name="Book",
            fields=[
                ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
                ("title", models.CharField(max_length=200)),
                ("author", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name="books", to="catalog.author")),
            ],
        ),
    ]

Things to notice:

  • Read the summary printed by makemigrations (+ create, ~ alter, - remove, p Python, s SQL, ? mixed). The symbols were added in Django 5.1; they come from each operation’s category.

  • sqlmigrate needs a database connection, because Django resolves constraint names against it. Run it against a copy of the database you will migrate, and run it for every migration a DBA wants to review. --backwards prints the SQL of the reverse direction.

  • makemigrations --check exits with status 1 when model changes lack a migration (and, since Django 4.2, it no longer writes the missing files). Put it in CI so that a forgotten migration fails the build.

  • Commit the model change and its migration together; otherwise a colleague who pulls half of the change gets a database that does not match the code.

  • --name gives a migration a meaningful name instead of an autogenerated one such as 0002_book_slug; the name must be a valid Python identifier. --no-header omits the "Generated by Django" line and timestamp.

Reading the plan before applying it

migrate --plan and showmigrations --plan show what is about to happen without touching the database, which is what you want in a deployment pipeline. migrate --check exits non-zero when unapplied migrations exist (added in Django 3.1), which makes it a readiness check.

python manage.py migrate catalog --plan
python manage.py showmigrations --plan catalog
python manage.py migrate --check; echo "exit status: $?"
Planned operations:
catalog.0002_book_slug
    Add field slug to book
catalog.0003_populate_book_slugs
    Raw Python operation
catalog.0004_book_slug_unique
    Alter field slug on book
[X]  catalog.0001_initial
[ ]  catalog.0002_book_slug
[ ]  catalog.0003_populate_book_slugs
[ ]  catalog.0004_book_slug_unique
exit status: 1

showmigrations --list (the default) groups migrations per app and, at -v 2, shows when each was applied. Apps without migrations are listed with (no migrations).

Anatomy of a migration file

What Django looks for is a subclass of django.db.migrations.Migration named Migration. Its useful attributes are these.

Table 2. Attributes of the Migration class
Attribute Meaning

dependencies

Migrations that must be applied first, as (app_label, migration_name) tuples, including the previous migration of the same app. Cross-app foreign keys add dependencies automatically.

operations

The ordered list of Operation objects: the schema and data changes.

run_before

Migrations that must run after this one, for cases where you cannot edit the other migration’s dependencies (for example to make a third-party app’s migrations run after your AUTH_USER_MODEL replacement). Prefer dependencies.

initial

True on the migration that creates the first version of an app’s tables. Without it, a migration with no dependency on another migration of its own app counts as initial. Used by migrate --fake-initial.

atomic

True by default: on databases with DDL transactions (SQLite and PostgreSQL) the whole migration runs in one transaction. Set atomic = False for non-transactional work.

replaces

Present on squashed migrations: the list of migrations this one stands in for.

swappable_dependency(…​)

A function (not an attribute) used inside dependencies to depend on the app of a swappable model such as the custom user (see below).

Operations accept a few shared options too. elidable=True on RunPython and RunSQL lets the optimiser drop the operation when squashing (see Squashing migrations), and hints is passed to the database routers (see Multiple databases).

The operations at a glance

All the core operations live in django.db.migrations.operations and are re-exported from django.db.migrations.

Table 3. Schema operations
Operation Effect Notes

CreateModel(name, fields, options, bases, managers)

Creates the model and its table.

fields are unbound field instances.

DeleteModel(name)

Drops the model and its table.

RenameModel(old_name, new_name)

Renames the model (and its table).

Not autodetected when many fields change at once: it looks like a delete plus a create and loses the data.

AlterModelTable, AlterModelTableComment

Change db_table or db_table_comment.

AlterUniqueTogether

Changes unique_together.

Prefer UniqueConstraint and AddConstraint.

AlterIndexTogether

Changes index_together.

Only for migration files from before Django 4.2; use AddIndex and RemoveIndex.

AlterOrderWithRespectTo

Adds or removes the _order column.

AlterModelOptions, AlterModelManagers

Record changes to Meta options (such as ordering, verbose_name) and to managers.

No database change; they keep the historical models used by RunPython accurate.

AddField(model_name, name, field, preserve_default=True)

Adds a column (or the table of a ManyToManyField).

preserve_default=False means the default only fills existing rows and is not kept in the state.

RemoveField

Drops the column.

Irreversible when the field is not nullable and has no default. Since Django 6.0 PostgreSQL no longer uses CASCADE: remove dependent objects such as views yourself.

AlterField

Changes type, null, unique, db_column, and so on.

Not every change is possible on every database (for example text to integer on most).

RenameField

Renames the field and, unless db_column is set, the column.

AddIndex, RemoveIndex, RenameIndex

Index management.

RenameIndex since 4.1; on SQLite it drops and recreates the index.

AddConstraint, RemoveConstraint, AlterConstraint

Constraint management.

AlterConstraint (5.2) changes a constraint without dropping and recreating it in the database.

Table 4. Special operations
Operation Purpose

RunSQL(sql, reverse_sql, state_operations, hints, elidable)

Arbitrary SQL, with an optional reverse and optional state operations.

RunPython(code, reverse_code, atomic, hints, elidable)

Python in a historical context: data migrations. RunPython.noop is the "do nothing" callable for either direction.

SeparateDatabaseAndState(database_operations, state_operations)

Different operations for the database and for the model state.

The django.contrib.postgres.operations module adds CreateExtension (and specific subclasses such as BloomExtension), AddIndexConcurrently, RemoveIndexConcurrently, AddConstraintNotValid and ValidateConstraint; they are covered in Zero-downtime migrations and in PostgreSQL & Database Backends.

Dependencies and the migration graph

Migrations are per app, but the tables they create are not independent. A ForeignKey from catalog to authors needs the authors table to exist first, so the migration that adds it declares a dependency on a migration of authors. Django does not use file names or numbers for ordering: it builds a directed graph from dependencies and run_before and applies the migrations in a topological order. The numeric prefix is only a convenience for humans.

flowchart TD subgraph catalog c1["catalog.0001_initial"] --> c2["catalog.0002_book_slug"] --> c3["catalog.0003_populate_book_slugs"] end subgraph auth a1["auth.0001_initial"] --> a2["auth.0012_..."] end a1 --> c4["catalog.0004_book_owner
(ForeignKey to AUTH_USER_MODEL)"] c3 --> c4

Consequences worth knowing:

  • Restricting makemigrations or migrate to one app label is a best-effort promise: any other app needed to get the dependencies right is included.

  • Unapplying a migration also unapplies every migration that depends on it, whatever the app. Use migrate --plan to see the list before you reverse anything.

  • Apps without migrations must not have relations to apps with migrations. It may seem to work, but it is not supported.

  • If a RunPython uses a model from another app, add the latest migration of that app to dependencies, otherwise apps.get_model() raises LookupError: No installed app with label …​.

Swappable dependencies

When a model points at settings.AUTH_USER_MODEL, the autodetector writes migrations.swappable_dependency(settings.AUTH_USER_MODEL), which depends on the first migration of the app that holds the swapped-in model. For this to work the swappable model must be created in that app’s initial migration. The generated file for a nullable owner on Book looks like this (generated in the project with the built-in User; only the name of the first dependency depends on your own history):

import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ("catalog", "0013_title_words_not_null"),
        migrations.swappable_dependency(settings.AUTH_USER_MODEL),
    ]

    operations = [
        migrations.AddField(
            model_name="book",
            name="owner",
            field=models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, to=settings.AUTH_USER_MODEL),
        ),
    ]

Set AUTH_USER_MODEL before the first migrate of a new project (see Custom User Model & Backends), because changing it later means rewriting history.

Using models from another app, and third-party apps that may disappear

When moving data between apps, the dependency on the old app is conditional, and get_model must tolerate its absence, so that the migration runs on projects where the old app was never installed:

from django.apps import apps as global_apps
from django.db import migrations


def forwards(apps, schema_editor):
    try:
        OldModel = apps.get_model("old_app", "OldModel")
    except LookupError:
        return  # the old app is not installed: nothing to move
    NewModel = apps.get_model("new_app", "NewModel")
    NewModel.objects.bulk_create(
        NewModel(new_attribute=old.old_attribute) for old in OldModel.objects.all()
    )


class Migration(migrations.Migration):
    operations = [migrations.RunPython(forwards, migrations.RunPython.noop)]
    dependencies = [
        ("myapp", "0123_the_previous_migration"),
        ("new_app", "0001_initial"),
    ]
    if global_apps.is_installed("old_app"):
        dependencies.append(("old_app", "0001_initial"))

This is the documented pattern; it is reproduced as a template and was not executed because it needs two apps that do not exist in the Bookshelf project.

Merging conflicting migrations

Two developers can each create 0007_…​ from the same 0006. The graph then has two leaf nodes in one app, and Django refuses to continue:

CommandError: Conflicting migrations detected; multiple leaf nodes in the migration graph: (0007_book_isbn, 0007_book_page_count in catalog).
To fix them run 'python manage.py makemigrations --merge'

makemigrations --merge writes an empty migration that depends on both leaves. If the two branches touched different things (as here: one added page_count, the other isbn) the merge is safe and automatic. If they conflict, you edit the dependencies by hand to linearise them.

python manage.py makemigrations --merge --noinput
python manage.py showmigrations --plan catalog
Merging catalog
  Branch 0007_book_isbn
    + Add field isbn to book
  Branch 0007_book_page_count
    + Add field page_count to book

Created new merge migration .../catalog/migrations/0008_merge_0007_book_isbn_0007_book_page_count.py
[X]  catalog.0006_book_title_index
[ ]  catalog.0007_book_page_count
[ ]  catalog.0007_book_isbn
[ ]  catalog.0008_merge_0007_book_isbn_0007_book_page_count
# catalog/migrations/0008_merge_0007_book_isbn_0007_book_page_count.py
from django.db import migrations


class Migration(migrations.Migration):

    dependencies = [
        ("catalog", "0007_book_isbn"),
        ("catalog", "0007_book_page_count"),
    ]

    operations = []

Alternative to a merge file: linearise by hand by changing the second branch’s dependency to the first one’s name (dependencies = [("catalog", "0007_book_page_count")] in 0007_book_isbn.py), which is what you must do when the branches really are ordered. If either branch contains a data migration, think about whether the order matters before accepting the automatic merge. Squashing the history later removes the clutter.

Related is the consistency check: if you edit dependencies and a migration is recorded as applied while one of its dependencies is not, Django refuses to run or create migrations until you fix the dependencies (InconsistentMigrationHistory).

Schema changes that need a hand-written migration

Adding a unique, non-nullable field to a table with rows

A plain AddField for a unique non-nullable field fails on a table with existing rows: the default is computed once and every row would get the same value. Django’s documented answer is three migrations: add the field as nullable, populate it, then tighten it. The example adds a unique slug to Book.

# catalog/models.py (step 1: the field is nullable and not unique yet)
class Book(models.Model):
    title = models.CharField(max_length=200)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
    slug = models.SlugField(max_length=220, null=True)
python manage.py makemigrations catalog --name book_slug
python manage.py sqlmigrate catalog 0002
python manage.py makemigrations catalog --empty --name populate_book_slugs
BEGIN;
--
-- Add field slug to book
--
ALTER TABLE "catalog_book" ADD COLUMN "slug" varchar(220) NULL;
CREATE INDEX "catalog_book_slug_4eebf644" ON "catalog_book" ("slug");
COMMIT;

Step 2 is a data migration (see the next section for the details of RunPython):

# catalog/migrations/0003_populate_book_slugs.py
from django.db import migrations
from django.utils.text import slugify


def populate_slugs(apps, schema_editor):
    # Historical model: the version of Book as of migration 0002.
    Book = apps.get_model("catalog", "Book")
    db_alias = schema_editor.connection.alias
    for book in Book.objects.using(db_alias).filter(slug__isnull=True):
        # The id suffix keeps the slugs unique ("The Dispossessed" appears twice).
        book.slug = f"{slugify(book.title)}-{book.pk}"
        book.save(update_fields=["slug"])


def clear_slugs(apps, schema_editor):
    Book = apps.get_model("catalog", "Book")
    Book.objects.using(schema_editor.connection.alias).update(slug=None)


class Migration(migrations.Migration):

    dependencies = [
        ("catalog", "0002_book_slug"),
    ]

    operations = [
        migrations.RunPython(populate_slugs, reverse_code=clear_slugs),
    ]

Step 3 changes the model to unique=True without null=True. Running makemigrations now asks a question, because the database needs something to put in existing NULL rows:

It is impossible to change a nullable field 'slug' on book to non-nullable without providing a default. This is because the database needs something to populate existing rows.
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Ignore for now. Existing rows that contain NULL values will have to be handled manually, for example with a RunPython or RunSQL operation.
 3) Quit and manually define a default value in models.py.
Select an option:

Because step 2 already filled every row, write the last migration by hand (makemigrations --empty) rather than answering "1", which would put the same value in every row:

# catalog/migrations/0004_book_slug_unique.py
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ("catalog", "0003_populate_book_slugs"),
    ]

    operations = [
        migrations.AlterField(
            model_name="book",
            name="slug",
            field=models.SlugField(max_length=220, unique=True),
        ),
    ]
python manage.py makemigrations --check --dry-run
python manage.py migrate catalog
python manage.py sqlmigrate catalog 0004
No changes detected
Operations to perform:
  Apply all migrations: catalog
Running migrations:
  Applying catalog.0002_book_slug... OK
  Applying catalog.0003_populate_book_slugs... OK
  Applying catalog.0004_book_slug_unique... OK
BEGIN;
--
-- Alter field slug on book
--
CREATE TABLE "new__catalog_book" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "slug" varchar(220) NOT NULL UNIQUE, "title" varchar(200) NOT NULL, "author_id" bigint NOT NULL REFERENCES "catalog_author" ("id") DEFERRABLE INITIALLY DEFERRED);
INSERT INTO "new__catalog_book" ("id", "title", "author_id", "slug") SELECT "id", "title", "author_id", coalesce("slug", NULL) FROM "catalog_book";
DROP TABLE "catalog_book";
ALTER TABLE "new__catalog_book" RENAME TO "catalog_book";
CREATE INDEX "catalog_book_author_id_b0849980" ON "catalog_book" ("author_id");
COMMIT;

The last SQL block shows a SQLite limitation: it cannot alter a column in place, so Django creates a new table, copies the rows, drops the old table and renames the new one (the same emulation the migrations guide describes). PostgreSQL would issue ALTER TABLE …​ ALTER COLUMN …​ SET NOT NULL and add the unique constraint. This is also why migrating on SQLite in production is discouraged.

There is a race to be aware of: rows created after the AddField and before the data migration finishes are overwritten, so run these migrations while writes are paused, or use the batching and ordering techniques of Zero-downtime migrations.

Renaming a field without losing data

If you rename a field and nothing else changes, the autodetector asks whether it was a rename and, when you say yes, writes RenameField instead of dropping and re-adding the column. The prompt is interactive. Run with --noinput, the question is answered "no": if the new field is not nullable and has no default, makemigrations refuses (Field 'display_name' on model 'author' not migrated: it is impossible to add a non-nullable field without specifying a default), but if the new field is nullable it silently writes RemoveField plus AddField (- Remove field full_name from author, + Add field display_name to author) and the data is lost. Without a terminal (CI) and without --noinput, the prompt fails with EOFError.

# models.py: Author.name becomes Author.full_name
echo y | python manage.py makemigrations catalog --name rename_author_name
python manage.py sqlmigrate catalog 0005
Was author.name renamed to author.full_name (a CharField)? [y/N] Migrations for 'catalog':
  catalog/migrations/0005_rename_author_name.py
    ~ Rename field name on author to full_name
BEGIN;
--
-- Rename field name on author to full_name
--
ALTER TABLE "catalog_author" RENAME COLUMN "name" TO "full_name";
COMMIT;
migrations.RenameField(model_name="author", old_name="name", new_name="full_name")

The same applies to RenameModel, which the autodetector offers only when little else about the model changed; if you rename a model and several of its fields at once, split the work into two migrations, or add the RenameModel by hand. Since Django 6.1, renaming a model through a migration also renames its Permission.name and Permission.codename values.

Reversibility

migrate can go backwards: pass the target migration, or zero to unapply everything the app has applied. Each operation knows how to reverse itself (a CreateModel reverses to a drop, an AddField to a drop of the column), with some limits that matter.

python manage.py migrate catalog 0002
python manage.py showmigrations catalog
python manage.py migrate catalog
Operations to perform:
  Target specific migration: 0002_book_slug, from catalog
Running migrations:
  Rendering model states... DONE
  Unapplying catalog.0004_book_slug_unique... OK
  Unapplying catalog.0003_populate_book_slugs... OK
catalog
 [X] 0001_initial
 [X] 0002_book_slug
 [ ] 0003_populate_book_slugs
 [ ] 0004_book_slug_unique
Operations to perform:
  Apply all migrations: catalog
Running migrations:
  Applying catalog.0003_populate_book_slugs... OK
  Applying catalog.0004_book_slug_unique... OK
  • A migration is irreversible if any of its operations is. RunPython without reverse_code and RunSQL without reverse_sql are irreversible; RemoveField of a non-nullable field with no default is too.

  • Pass RunPython.noop or RunSQL.noop as the reverse when "undo" really is "nothing to do". It documents the intent and keeps the migration reversible (in the slug example, the schema reverse already removes the column, so an explicit clear_slugs was optional).

  • Reversing drops data that the forward direction created. A reversible migration is not a backup.

  • All dependent migrations are unapplied first, regardless of app.

Trying to go back past an irreversible operation raises IrreversibleError. This migration creates a table with RunSQL and no reverse:

# catalog/migrations/0005_irreversible_demo.py (demonstration only; removed afterwards)
from django.db import migrations


class Migration(migrations.Migration):
    dependencies = [("catalog", "0004_book_slug_unique")]
    operations = [
        migrations.RunSQL("CREATE TABLE demo_books (id integer PRIMARY KEY)"),
    ]
django.db.migrations.exceptions.IrreversibleError: Operation <RunSQL 'CREATE TABLE demo_books (id integer PRIMARY KEY)'> in catalog.0005_irreversible_demo is not reversible

The fix is to add reverse_sql="DROP TABLE demo_books" (or RunSQL.noop).

Data migrations with RunPython

Migrations can change data as well as schema. Django cannot generate data migrations, but creating one is short: makemigrations --empty app writes a migration with the right dependencies, and you add a RunPython operation that takes two callables, code and optionally reverse_code. Each receives two arguments: an apps registry with the historical models for that point in the history, and the schema_editor.

Historical models

When migrations run, Django works from the versions of the models stored in the migration files, not from your current models.py. That is why a data migration must call apps.get_model() and never import the model:

  • If you import the real model, the migration works today and breaks later, when a new installation replays old migrations against a model that has since gained fields, lost fields or changed constraints. The failure is silent until then.

  • Historical models have the same fields, relationships, Meta options (versioned too) and managers that set use_in_migrations = True. They do not have your custom methods, custom save(), custom constructors or properties: plan for it.

  • Base classes are stored as pointers and field options such as upload_to and limit_choices_to as references to functions, so those classes and functions must be kept as long as a migration refers to them. Squashing removes old references.

The test in Testing migrations proves the point: in the historical Author model, str is Django’s default (Author object (1)) rather than your method.

To make a manager available in migrations, declare it with use_in_migrations = True; Django then serializes it into an AlterModelManagers operation:

class BookManager(models.Manager):
    use_in_migrations = True

    def get_queryset(self):
        return super().get_queryset().order_by("title")


class Book(models.Model):
    objects = BookManager()
    # ...
Migrations for 'catalog':
  catalog/migrations/0016_alter_book_managers.py
    ~ Change managers on book
migrations.AlterModelManagers(
    name="book",
    managers=[("objects", catalog.models.BookManager())],
)

The consequence is that the manager class must stay importable for as long as that migration exists. If you build it with from_queryset(), inherit from the generated class so that it can be imported. See Managers & Custom QuerySets.

Writing the function well

  • Keep the function above the Migration class, with a name that says what it does.

  • Use schema_editor.connection.alias as the database alias (Model.objects.using(db_alias)). Model methods otherwise go to the default database, which is wrong when migrating another one.

  • Prefer bulk_create, bulk_update and QuerySet.update() over row-by-row save() for large tables. Row-by-row save() in the slug migration above is fine for a few thousand rows; the batched variant in Zero-downtime migrations scales further.

  • Write data migrations as separate migrations that sit next to the schema migrations, so they can be reverted and squashed independently.

  • Mixing a schema change and a RunPython in the same migration on PostgreSQL can raise OperationalError: cannot ALTER TABLE "…​" because it has pending trigger events, because both run in the same transaction. Split them into two migrations.

  • Migrations run synchronously, and the RunPython callables receive a synchronous schema_editor: use the synchronous ORM in them.

migrations.RunPython(forwards, reverse_code=backwards, atomic=None, hints=None, elidable=False)

atomic defaults to None: on databases without DDL transactions (MySQL, Oracle) the operation runs in a transaction, which is safe but may crash if you use the schema_editor there; pass atomic=False in that case. On SQLite and PostgreSQL, the operation adds no transaction beyond the migration’s own, unless the migration is non-atomic and you pass atomic=True.

Source: RunPython.

RunSQL, and keeping the state in sync

RunSQL runs SQL the ORM cannot express (triggers, extensions, database-specific features). Pass reverse_sql to make it reversible, and state_operations to tell the autodetector what the SQL did to the model state; otherwise the next makemigrations would want to do it again. The next migration creates the index of a Meta.indexes entry by hand:

class Book(models.Model):
    # ...
    class Meta:
        indexes = [models.Index(fields=["title"], name="book_title_idx")]
# catalog/migrations/0006_book_title_index.py
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ("catalog", "0005_rename_author_name"),
    ]

    operations = [
        migrations.RunSQL(
            sql='CREATE INDEX "book_title_idx" ON "catalog_book" ("title")',
            reverse_sql='DROP INDEX "book_title_idx"',
            state_operations=[
                migrations.AddIndex(
                    model_name="book",
                    index=models.Index(fields=["title"], name="book_title_idx"),
                ),
            ],
        ),
    ]
python manage.py makemigrations --check --dry-run
python manage.py sqlmigrate catalog 0006 --backwards
No changes detected
BEGIN;
--
-- Raw SQL operation
--
DROP INDEX "book_title_idx";
COMMIT;

sql may be a string, a list of strings or a list of (statement, params) tuples (use %s placeholders, and double literal percent signs when passing parameters). On every backend but PostgreSQL Django splits a multi-statement string into separate statements. Do not put BEGIN or COMMIT in the SQL of an atomic migration on PostgreSQL or SQLite; it breaks Django’s transaction handling. Hand-written SQL is not portable, so when you have more than one database in the matrix, branch on schema_editor.connection.vendor or keep the SQL in a dedicated migration.

SeparateDatabaseAndState

SeparateDatabaseAndState is the escape hatch for changes where what the database must do and what the model state must record are different. It takes two lists: database_operations, used when applying SQL, and state_operations, used when computing the state. It is a generalised version of RunSQL’s `state_operations argument. The two classic cases are renaming a table by hand to keep its data when Django would drop and recreate it, and recording a change that already happened in the database.

The documented example turns the automatic ManyToManyField of Book and Tag into one with a through model that carries an extra field. The default migration would delete the catalog_book_tags table and create a new one, losing the links. Instead, rename the table in the database and describe the new model only in the state.

# models.py (after the change)
class Book(models.Model):
    # ...
    tags = models.ManyToManyField("Tag", through="BookTag", blank=True)


class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)


class BookTag(models.Model):
    book = models.ForeignKey(Book, on_delete=models.CASCADE)
    tag = models.ForeignKey(Tag, on_delete=models.CASCADE)
    is_primary = models.BooleanField(default=False)

    class Meta:
        constraints = [
            models.UniqueConstraint(fields=["book", "tag"], name="unique_book_tag"),
        ]

The old table name comes from sqlmigrate or dbshell; the new one is BookTag._meta.db_table. The foreign keys keep the names Django used (book_id, tag_id), and any extra field is added after SeparateDatabaseAndState:

# catalog/migrations/0010_book_tags_through.py
import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ("catalog", "0009_tag_and_book_tags"),
    ]

    operations = [
        migrations.SeparateDatabaseAndState(
            database_operations=[
                # Old name: from sqlmigrate/dbshell. New name: BookTag._meta.db_table.
                migrations.RunSQL(
                    sql="ALTER TABLE catalog_book_tags RENAME TO catalog_booktag",
                    reverse_sql="ALTER TABLE catalog_booktag RENAME TO catalog_book_tags",
                ),
            ],
            state_operations=[
                migrations.CreateModel(
                    name="BookTag",
                    fields=[
                        ("id", models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name="ID")),
                        ("book", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="catalog.book")),
                        ("tag", models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to="catalog.tag")),
                    ],
                    options={
                        "constraints": [
                            models.UniqueConstraint(fields=["book", "tag"], name="unique_book_tag")
                        ],
                    },
                ),
                migrations.AlterField(
                    model_name="book",
                    name="tags",
                    field=models.ManyToManyField(blank=True, through="catalog.BookTag", to="catalog.tag"),
                ),
            ],
        ),
        migrations.AddField(
            model_name="booktag",
            name="is_primary",
            field=models.BooleanField(default=False),
        ),
    ]
python manage.py makemigrations --check --dry-run -v3
python manage.py migrate catalog
python manage.py shell -c "
from catalog.models import BookTag
print([(bt.book.title, bt.tag.name, bt.is_primary) for bt in BookTag.objects.all()])"
python manage.py migrate catalog 0009
No changes detected
Running migrations:
  Applying catalog.0010_book_tags_through... OK
[('Kindred', 'time travel', False), ('The Dispossessed', 'utopia', False)]
Running migrations:
  Rendering model states... DONE
  Unapplying catalog.0010_book_tags_through... OK

The existing rows survived (the links were created with the old automatic table), the autodetector reports no pending changes, and the migration is reversible because the RunSQL has a reverse. The sqlmigrate output shows the rename followed by the table rebuild that adds is_primary on SQLite. Two details to check on your database: the old unique index of the automatic table keeps its old name after the rename, and the unique_book_tag constraint is only in the state. On SQLite the AddField that follows rebuilds the table and creates the constraint (visible in the sqlmigrate output), but on a database that alters the table in place no step creates it, so add the constraint to the database explicitly if you need it enforced under the new name.

Other uses follow the same shape: moving a model between apps (database_operations rename the table, state_operations delete the model in one app and create it in the other), changing Meta.managed, and the PostgreSQL case in the Django 4.1 release notes where only the state changes (database_operations=[] with RemoveConstraint and AddConstraint in state_operations). If the state and the database drift apart, the migration framework can break and even lose data, so check database_operations with sqlmigrate and dbshell, and state_operations with makemigrations --dry-run. A final caution from the guide: if you change an unmanaged model (managed = False) to managed, remove managed = False and generate that migration before making other schema changes to the model, because schema changes in the same migration may not be applied.

Squashing migrations

Hundreds of migrations are not a performance problem (the loader is optimised for them), but they slow down fresh databases and test runs and clutter the repository. squashmigrations reads the operations of a range of migrations, runs the optimiser over them (CreateModel followed by DeleteModel cancels out, AddField folds into CreateModel) and writes a new migration whose replaces attribute lists the originals. Both can coexist: databases part-way through the old range keep using the old files until they reach the end, and new databases use the squashed migration.

python manage.py squashmigrations catalog 0001 0004 --noinput
Will squash the following migrations:
 - 0001_initial
 - 0002_book_slug
 - 0003_populate_book_slugs
 - 0004_book_slug_unique
Optimizing...
  Optimized from 5 operations to 4 operations.
Created new squashed migration .../catalog/migrations/0001_initial_squashed_0004_book_slug_unique.py
  You should commit this migration but leave the old ones in place;
  the new migration will be used for new installs. Once you are sure
  all instances of the codebase have applied the migrations you squashed,
  you can delete them.
Manual porting required
  Your migrations contained functions that must be manually copied over,
  as we could not safely copy their implementation.
  See the comment at the top of the squashed migration for details.

The last lines matter. RunPython operations cannot be optimised through (unless they are marked elidable), and Django cannot copy functions into the new file, so it writes a reference that is not valid Python:

# Functions from the following migrations need manual copying.
# Move them and any dependencies into this file, then update the
# RunPython operations to refer to the local versions:
# catalog.migrations.0003_populate_book_slugs

class Migration(migrations.Migration):

    replaces = [('catalog', '0001_initial'), ('catalog', '0002_book_slug'), ('catalog', '0003_populate_book_slugs'), ('catalog', '0004_book_slug_unique')]
    # ...
        migrations.RunPython(
            code=catalog.migrations.0003_populate_book_slugs.populate_slugs,
            reverse_code=catalog.migrations.0003_populate_book_slugs.clear_slugs,
        ),

Until you port the functions, every manage.py command fails with SyntaxError: invalid decimal literal. Copy populate_slugs and clear_slugs into the squashed file and refer to them locally (code=populate_slugs). Two ways to avoid the work: squash only the range after your last data migration by passing the optional start_migration_name (squashmigrations catalog 0004 0010), or mark throw-away data migrations elidable=True so that squashing drops them.

After the manual port, the state is consistent:

python manage.py showmigrations catalog
python manage.py makemigrations --check
catalog
 [-] 0001_initial_squashed_0004_book_slug_unique (4 squashed migrations) Run 'manage.py migrate' to finish recording.
 [X] 0005_rename_author_name
 ...
No changes detected

Running migrate records the squashed migration as applied on a database that had the whole range applied. On a fresh database only the squashed migration runs, in one step:

Running migrations:
  Applying catalog.0001_initial_squashed_0004_book_slug_unique... OK
  Applying catalog.0005_rename_author_name... OK
  ...

Finishing the squash

The recommended process is a two-release dance. Squash and keep the old files, commit and release, wait until every environment (including every third-party user of your app) has applied the new release, then transition the squashed migration into a normal one:

  1. Delete the files of the migrations it replaces.

  2. Update every migration that depended on the deleted ones to depend on the squashed migration instead.

  3. Remove the replaces attribute from the squashed migration.

  4. Run migrate --prune for the app, to delete the references to the removed migrations from the django_migrations table. It is useful when the names of deleted migrations might be reused, and it requires an app label.

python manage.py migrate catalog --prune
python manage.py migrate --check; echo "exit status: $?"
Pruning migrations:
  Pruning catalog.0001_initial OK
  Pruning catalog.0002_book_slug OK
  Pruning catalog.0003_populate_book_slugs OK
  Pruning catalog.0004_book_slug_unique OK
exit status: 0

Running migrate --prune without an app label fails with Migrations can be pruned only when an app is specified. Since Django 6.0 a squashed migration can itself be squashed, before being transitioned to a normal migration; this helps when not every environment has applied the first squash yet. Squashing may also produce migrations that do not run: retry with --no-optimize (and report a bug), or, for a CircularDependencyError, break one foreign key of the loop out into a separate migration by hand.

Two helpers that shrink migrations without squashing

  • optimizemigration app_label name (Django 4.1) optimises the operations of one migration, overwriting the file. When it contains functions that must be copied manually it writes a new file suffixed _optimized instead. --check exits non-zero when the migration can be optimised, which is handy as a lint.

  • makemigrations --update (Django 4.2) folds new model changes into the latest migration and optimises the result, instead of creating a new file. Use it while a migration is still local and unapplied; the updated migration gets a generated name (--name keeps one).

# catalog/migrations/0017_two_ops.py: two operations touching the same new field
operations = [
    migrations.AddField("book", "subtitle", models.CharField(max_length=50, default="")),
    migrations.AlterField("book", "subtitle", models.CharField(max_length=120, default="")),
]
python manage.py optimizemigration catalog 0017
Optimizing from 2 operations to 1 operations.
Optimized migration .../catalog/migrations/0017_two_ops.py
operations = [
    migrations.AddField(
        model_name="book",
        name="subtitle",
        field=models.CharField(default="", max_length=120),
    ),
]
# change Book.notes max_length from 50 to 200 before the first migration was applied
python manage.py makemigrations catalog --update
Migrations for 'catalog':
  catalog/migrations/0016_book_notes_updated.py
    + Add field notes to book
Deleted catalog/migrations/0016_book_notes.py

Non-atomic migrations

On databases with DDL transactions (SQLite and PostgreSQL) a migration runs inside a single transaction by default, so a failure leaves nothing half-applied. On databases without them (MySQL, Oracle) every operation runs on its own, and a failed migration has to be repaired by hand, because MySQL’s atomic DDL is per statement, not per migration. For long data migrations on big tables a single transaction is a liability: it holds locks and a snapshot for the whole run. Set atomic = False to run the operations without a surrounding transaction. You can still open transactions where you want them, with transaction.atomic() or RunPython(…​, atomic=True).

The next migration backfills a new title_words column in batches, one short transaction per batch:

# catalog/migrations/0012_backfill_title_words.py
from django.db import migrations, transaction

BATCH_SIZE = 2  # use a few thousand on a real table


def backfill(apps, schema_editor):
    Book = apps.get_model("catalog", "Book")
    db_alias = schema_editor.connection.alias
    last_pk = 0
    while True:
        batch = list(
            Book.objects.using(db_alias)
            .filter(pk__gt=last_pk, title_words__isnull=True)
            .order_by("pk")[:BATCH_SIZE]
        )
        if not batch:
            break
        # One short transaction per batch instead of one for the whole table.
        with transaction.atomic(using=db_alias):
            for book in batch:
                book.title_words = len(book.title.split())
            Book.objects.using(db_alias).bulk_update(batch, ["title_words"])
        last_pk = batch[-1].pk


class Migration(migrations.Migration):
    atomic = False

    dependencies = [
        ("catalog", "0011_book_title_words"),
    ]

    operations = [
        migrations.RunPython(backfill, reverse_code=migrations.RunPython.noop),
    ]

The loop pages by primary key (pk__gt=last_pk) instead of re-querying isnull rows from the start and ends when a batch is empty. It is restartable: if it fails half-way, the rows already updated stay updated and the next run only touches the rest. The atomic attribute has no effect on databases without DDL transactions (MySQL, Oracle). Also, in a non-atomic migration, only use BEGIN/COMMIT in RunSQL on SQLite and PostgreSQL when you really mean it.

Zero-downtime migrations

During a rolling deployment, old and new code run at the same time against the same database, and a schema change can lock a table for as long as it takes to rewrite it. Django has no switch for "zero downtime": it is a discipline of small, backward-compatible steps, often called expand and contract.

sequenceDiagram participant Dev as Release N participant DB as Database participant Old as Old code participant New as New code Dev->>DB: Expand: add the new column as NULLable Old->>DB: still works (ignores the column) Dev->>DB: Backfill in batches (non-atomic migration) New->>DB: writes both columns, reads the new one Dev->>DB: Contract (release N+1): NOT NULL, drop the old column
  1. Expand. Add the new column (or table) as nullable and without a default that forces a rewrite. Old code ignores it.

  2. Dual write. Deploy code that writes the old and the new columns and reads the new one (with a fallback).

  3. Backfill. Fill the new column for old rows, in batches, in a non-atomic migration (above).

  4. Contract. In the following release, tighten the column (NOT NULL, constraint) and then drop the old column. Never drop a column that running code still reads.

The example makes title_words mandatory once it is backfilled. The model drops null=True and a hand-written migration tightens the column:

# catalog/migrations/0013_title_words_not_null.py
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ("catalog", "0012_backfill_title_words"),
    ]

    operations = [
        migrations.AlterField(
            model_name="book",
            name="title_words",
            field=models.PositiveSmallIntegerField(),
        ),
    ]
python manage.py makemigrations --check
python manage.py migrate catalog
No changes detected
Running migrations:
  Applying catalog.0013_title_words_not_null... OK

Rules of thumb that follow from the documentation:

  • On older databases, adding a field with a default can rewrite the whole table, even for nullable fields. The AddField reference advises adding the nullable field without the default first and then adding the default in a second AlterField migration. preserve_default=False only affects what Django remembers, not the database default.

  • Index creation takes a lock that blocks writes. On PostgreSQL use AddIndexConcurrently and RemoveIndexConcurrently from django.contrib.postgres.operations; CONCURRENTLY cannot run in a transaction, so such a migration must set atomic = False.

  • To add a check constraint to a big PostgreSQL table without a long scan, use AddConstraintNotValid followed by ValidateConstraint in two separate migrations (doing both in one atomic migration is the same as a plain AddConstraint, and in one non-atomic migration may leave the database inconsistent if validation fails).

  • Do not rename columns or tables that running code uses; add the new name and migrate over the two releases above. RenameField and RenameModel are safe for the database but not for old code.

  • On PostgreSQL, consider a short lock_timeout for the session that runs the migration, so that an ALTER TABLE waiting for a lock fails fast instead of queueing every request behind it (a PostgreSQL setting, not a Django one; see PostgreSQL & Database Backends).

# PostgreSQL only: not run here (no PostgreSQL server was available)
from django.contrib.postgres.operations import AddIndexConcurrently
from django.db import migrations, models


class Migration(migrations.Migration):
    atomic = False  # CREATE INDEX CONCURRENTLY cannot run in a transaction

    dependencies = [("catalog", "0013_title_words_not_null")]

    operations = [
        AddIndexConcurrently(
            "book",
            models.Index(fields=["isbn"], name="book_isbn_idx"),
        ),
    ]

The general database-side practices (lock levels, online schema change tools, backfilling strategies and rollback plans) are covered, independently of Django, in Evolving the Database Model: Best Practices and Core Concepts; the tools that other stacks use to do the same job are compared in Choosing a Migration Tool.

Database support

Migrations run on every backend Django ships, but the databases are not equally capable.

Table 5. Backend differences that affect migrations
Database What to know

PostgreSQL

The most capable: transactional DDL (a failed migration rolls back completely), CONCURRENTLY operations, NOT VALID constraints, extensions through CreateExtension (Django 6.1 needs PostgreSQL 15 or later).

MySQL / MariaDB

No transactions around schema changes: if a migration fails you must unpick the changes by hand before retrying. MySQL 8 made many DDL operations cheaper but cannot promise the absence of locks. There is also a relatively small limit on the combined size of the columns an index covers.

SQLite

Very little built-in ALTER TABLE support, so Django emulates it by creating a new table, copying the data, dropping the old table and renaming the new one (visible in the sqlmigrate output above). This is slow on big tables and occasionally fragile, so Django recommends it for local development only.

Custom operations and the schema editor

When you repeat the same hand-written change in several migrations, write your own operation. An Operation subclass implements state_forwards (mutate the project state), database_forwards and, if reversible, database_backwards, and uses the schema_editor to execute SQL. The next operation creates and drops a database view and changes no model state.

# catalog/operations.py
from django.db.migrations.operations.base import Operation, OperationCategory


class CreateView(Operation):
    """Create a database view; reverse drops it. Does not change the model state."""

    reversible = True
    reduces_to_sql = True  # show the SQL in sqlmigrate
    category = OperationCategory.SQL

    def __init__(self, name, select_sql):
        self.name = name
        self.select_sql = select_sql

    def state_forwards(self, app_label, state):
        pass  # views are not models, so the project state does not change

    def database_forwards(self, app_label, schema_editor, from_state, to_state):
        schema_editor.execute(
            "CREATE VIEW %s AS %s"
            % (schema_editor.quote_name(self.name), self.select_sql)
        )

    def database_backwards(self, app_label, schema_editor, from_state, to_state):
        schema_editor.execute("DROP VIEW %s" % schema_editor.quote_name(self.name))

    def describe(self):
        return "Creates view %s" % self.name

    @property
    def migration_name_fragment(self):
        return "create_view_%s" % self.name

    def deconstruct(self):
        # Needed only if makemigrations/squashmigrations must write the operation out.
        return (self.__class__.__qualname__, [], {"name": self.name, "select_sql": self.select_sql})
# catalog/migrations/0014_book_titles_view.py
from django.db import migrations

from catalog.operations import CreateView


class Migration(migrations.Migration):

    dependencies = [
        ("catalog", "0013_title_words_not_null"),
    ]

    operations = [
        CreateView(
            "catalog_book_titles",
            "SELECT id, title, title_words FROM catalog_book",
        ),
    ]
python manage.py sqlmigrate catalog 0014
python manage.py migrate catalog
python manage.py migrate catalog 0013
BEGIN;
--
-- Creates view catalog_book_titles
--
CREATE VIEW "catalog_book_titles" AS SELECT id, title, title_words FROM catalog_book;
COMMIT;
Running migrations:
  Applying catalog.0014_book_titles_view... OK
  Rendering model states... DONE
  Unapplying catalog.0014_book_titles_view... OK

Notes from the reference:

  • reduces_to_sql is False by default, which makes sqlmigrate skip the operation; reversible defaults to False, which makes Django refuse to go back past it; category picks the symbol shown by makemigrations (OperationCategory since Django 5.1: ADDITION +, REMOVAL -, ALTERATION ~, PYTHON p, SQL s, MIXED ?).

  • Field instances in ModelState.fields (and managers in ModelState.managers) are shared across migrations for speed: never mutate their attributes; replace the instance in state_forwards.

  • If you use model classes from from_state in database_forwards, call from_state.clear_delayed_apps_cache() first so related models are available.

  • A views-and-tables hazard: on SQLite, an operation that rebuilds the table underneath a view fails, because the drop and rename leave the view pointing at a missing table. With the view above in place, adding an owner field to Book (which makes SQLite rebuild catalog_book) fails with OperationalError: error in view catalog_book_titles: no such table: main.catalog_book. Drop dependent views before such migrations and recreate them after. Django 6.0 made the PostgreSQL side of this explicit: RemoveField no longer uses CASCADE, so dependent objects Django does not manage must be removed manually before running it.

The schema editor, available as connection.schema_editor() (always as a context manager, which manages transactions and deferred SQL such as foreign-key constraints), exposes the same building blocks that operations use: create_model, delete_model, add_index, remove_index, rename_index, add_constraint, remove_constraint, alter_unique_together, alter_db_table, alter_db_table_comment, alter_db_tablespace, add_field, remove_field, alter_field and the low-level execute. You rarely need it outside migrations; it is useful in tests and tools that create temporary tables for unmanaged models:

from django.db import connection

with connection.schema_editor() as schema_editor:
    schema_editor.create_model(MyUnmanagedModel)

schema_editor.connection.alias gives the name of the database being migrated, which is what multi-database migrations use.

Serialization: what can go into a migration

To write a migration, Django must turn the current model definition into Python source. It can serialize most things: numbers, strings, bytes and None, lists, sets, tuples, dicts, ranges, dates and times (aware ones too), zoneinfo.ZoneInfo instances (Django 6.0), Decimal, Enum and Flag instances (Flag since 4.2), UUID, functools.partial, pathlib paths, TextChoices and IntegerChoices members, any Django field, functions and classes defined at module top level, functions decorated with functools.cache() or functools.lru_cache() (since 5.0), and anything with a deconstruct() method. It cannot serialize nested classes, lambdas or arbitrary instances.

A class you own becomes serializable with the @deconstructible decorator, plus an eq so that makemigrations does not see a change on every run:

# catalog/models.py
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.deconstruct import deconstructible


@deconstructible
class DigitsOnly:
    def __init__(self, length=13):
        self.length = length

    def __call__(self, value):
        if not (value.isdigit() and len(value) == self.length):
            raise ValidationError(f"Expected {self.length} digits.")

    def __eq__(self, other):
        return isinstance(other, DigitsOnly) and self.length == other.length


class Book(models.Model):
    isbn = models.CharField(max_length=13, blank=True, validators=[DigitsOnly(13)])
    # ...
Migrations for 'catalog':
  catalog/migrations/0016_alter_book_isbn.py
    ~ Alter field isbn on book
migrations.AlterField(
    model_name="book",
    name="isbn",
    field=models.CharField(blank=True, max_length=13, validators=[catalog.models.DigitsOnly(13)]),
)

With a lambda as validator instead, makemigrations fails when it writes the file:

ValueError: Cannot serialize function: lambda

Use a top-level function or a deconstructible class. The consequences of serialization are lasting: everything a migration references (validators, upload_to functions, custom fields, manager classes, base classes) must remain importable for as long as the migration exists, which is one more reason to squash. Other rules from the guide:

  • You cannot change the number of positional arguments of an already migrated custom field: old migrations call the new init with the old signature and raise TypeError. Add new arguments as keyword arguments with defaults.

  • To remove a custom field Django-style, first set system_check_deprecated_details on it, then after your deprecation period system_check_removed_details, and keep the stub (with init(), deconstruct() and get_internal_type()) until no migration references it.

  • If you maintain a reusable app, create migrations with the lowest Django version you support: migration files made on Django X.Y are meant to run unchanged on X.Y+1, but there is no forward compatibility.

  • To teach Django to write a type it does not know, register a serializer with MigrationWriter.register_serializer(type, serializer_class), where the serializer’s serialize() returns the source text and the set of imports it needs.

Multiple databases

With several databases, migrate works on one at a time: migrate --database archive applies the migrations to the archive entry of DATABASES, and each database has its own django_migrations table. A router’s allow_migrate(db, app_label, model_name=None, **hints) decides which migrations run where (see Transactions, Raw SQL & Multiple Databases). For RunPython and RunSQL, hints is passed to the router so that it can make a decision, and schema_editor.connection.alias tells the function which database it is running against. The next router and migration seed data on the default database only:

# catalog/dbrouters.py
class HintRouter:
    def allow_migrate(self, db, app_label, model_name=None, **hints):
        if "target_db" in hints:
            return db == hints["target_db"]
        return True
# config/settings.py
DATABASES["archive"] = {"ENGINE": "django.db.backends.sqlite3", "NAME": BASE_DIR / "archive.sqlite3"}
DATABASE_ROUTERS = ["catalog.dbrouters.HintRouter"]
# catalog/migrations/0015_seed_default_db_only.py
from django.db import migrations


def forwards(apps, schema_editor):
    Tag = apps.get_model("catalog", "Tag")
    # Always use the alias of the database being migrated.
    Tag.objects.using(schema_editor.connection.alias).get_or_create(name="seeded")


class Migration(migrations.Migration):

    dependencies = [
        ("catalog", "0014_book_titles_view"),
    ]

    operations = [
        migrations.RunPython(
            forwards,
            reverse_code=migrations.RunPython.noop,
            hints={"target_db": "default"},
        ),
    ]
python manage.py migrate
python manage.py migrate --database archive
python manage.py shell -c "
from catalog.models import Tag
print('default', list(Tag.objects.using('default').values_list('name', flat=True)))
print('archive', list(Tag.objects.using('archive').values_list('name', flat=True)))"
default ['utopia', 'time travel', 'seeded']
archive []

Both databases ended with the same schema, but only default received the seed row. If your RunPython or RunSQL touches a single model, pass model_name as a hint, which is especially important for reusable apps. Two more effects of multiple databases: makemigrations checks the consistency of the history on every database the routers allow, and tests that call migration commands must declare the databases they use (databases = {"default", "archive"} or databases = "all"), otherwise Django raises DatabaseOperationForbidden.

Testing migrations and CI

Three checks keep migrations honest. The first two are cheap and belong in every pipeline:

  • python manage.py makemigrations --check --dry-run fails when someone changed a model without a migration.

  • python manage.py migrate against an empty database (the test runner does this for you) proves the whole history replays. Reverse it too (migrate app zero) if you promise reversibility.

  • A migration test for each data migration, which seeds data at the old state, migrates and asserts the result. Django has no built-in helper; the building block is MigrationExecutor.

# catalog/tests.py
from django.core.management import call_command
from django.db import connection
from django.db.migrations.executor import MigrationExecutor
from django.test import TransactionTestCase


class MigrationTestCase(TransactionTestCase):
    """Base class: migrate to `migrate_from`, let the test seed data, then to `migrate_to`."""

    app = "catalog"
    migrate_from = None
    migrate_to = None

    def setUp(self):
        super().setUp()
        self.executor = MigrationExecutor(connection)
        self.executor.migrate([(self.app, self.migrate_from)])
        old_apps = self.executor.loader.project_state([(self.app, self.migrate_from)]).apps
        self.setUpBeforeMigration(old_apps)
        self.executor = MigrationExecutor(connection)  # reload the graph
        self.executor.migrate([(self.app, self.migrate_to)])
        self.apps = self.executor.loader.project_state([(self.app, self.migrate_to)]).apps

    def setUpBeforeMigration(self, apps):
        pass

    def tearDown(self):
        # Leave the test database fully migrated for the next test.
        call_command("migrate", verbosity=0)
        super().tearDown()


class PopulateSlugsTests(MigrationTestCase):
    migrate_from = "0002_book_slug"
    migrate_to = "0004_book_slug_unique"

    def setUpBeforeMigration(self, apps):
        Author = apps.get_model("catalog", "Author")
        Book = apps.get_model("catalog", "Book")
        author = Author.objects.create(name="Ursula K. Le Guin")
        Book.objects.create(title="The Dispossessed", author=author)
        Book.objects.create(title="The Dispossessed", author=author)

    def test_slugs_are_filled_and_unique(self):
        Book = self.apps.get_model("catalog", "Book")
        slugs = sorted(Book.objects.values_list("slug", flat=True))
        self.assertEqual(len(slugs), len(set(slugs)))
        self.assertTrue(all(s.startswith("the-dispossessed-") for s in slugs))

    def test_historical_models_have_no_custom_methods(self):
        Author = self.apps.get_model("catalog", "Author")
        # The real Author.__str__ returns the name; the historical one is Django's default.
        self.assertTrue(str(Author(pk=1)).startswith("Author object"))


class ReversibilityTests(MigrationTestCase):
    migrate_from = "0004_book_slug_unique"
    migrate_to = "0004_book_slug_unique"

    def test_every_migration_can_be_reversed_and_reapplied(self):
        call_command("migrate", "catalog", "zero", verbosity=0)
        call_command("migrate", "catalog", verbosity=0)


class NoMissingMigrationsTests(TransactionTestCase):
    def test_models_and_migrations_are_in_sync(self):
        # --check exits with status 1 (SystemExit) when model changes lack a migration.
        call_command("makemigrations", "--check", "--dry-run", verbosity=0)
python manage.py test catalog
Creating test database for alias 'default'...
....
----------------------------------------------------------------------
Ran 4 tests in 0.140s

OK
Destroying test database for alias 'default'...

(With a second database in DATABASES, as in the multi-database section, the last test class needs databases = {"default", "archive"}.) The TransactionTestCase base is required: migrations change the schema, which cannot happen inside the transaction that wraps a plain TestCase. The tests are slower than ordinary ones, so write them for risky data migrations and delete them once the migration is squashed away. Third-party helpers such as django-test-migrations exist for the same job, but the example above only uses what Django ships. For a project-wide view of test strategy see Testing.

A pipeline step list that works for most projects: makemigrations --check --dry-run, migrate on a throw-away database of the same engine as production, the test suite, migrate --plan (kept as a build artifact for review), and on deploy migrate --check as a readiness gate followed by migrate --noinput. Run migrations once per deploy, from one place (a release job, an init container or a one-off command), not from every web process at start-up; concurrent migrate runs race on the same tables. See Deployment.

Existing databases, fakes and unmanaged tables

A few commands and options change the bookkeeping without (or instead of) running SQL. They are sharp tools.

Table 6. Bookkeeping options of migrate
Option Use

--fake

Marks migrations up to the target as applied (or unapplied) without running SQL. For when you applied the SQL by hand. It can leave django_migrations in a state that needs manual repair.

--fake-initial

Skips an app’s initial migration if all the tables (for CreateModel) or columns (for AddField) it would create already exist. Only checks names, not the full schema.

--run-syncdb

Creates tables for apps without migrations. Not recommended; sometimes used when migrations are too slow for hundreds of models.

--prune

Removes records of nonexistent migrations (see squashing).

To bring an app that has models and tables but no migrations (for example, created before migrations existed) under migrations: run makemigrations your_app_label to create the initial migration, then migrate --fake-initial. This only works if you have not changed the models since you made their tables and have not edited the database by hand, because Django compares models with migration files, not with the database. The difference is easy to see:

# tables exist, but django_migrations has no catalog rows
python manage.py migrate catalog 0001
python manage.py migrate catalog 0001 --fake-initial
django.db.utils.OperationalError: table "catalog_author" already exists
  Target specific migration: 0001_initial, from catalog
Running migrations:
  Applying catalog.0001_initial... FAKED

--fake also reverses: migrate catalog 0001 --fake against a fully migrated database does not run any reverse SQL, it only marks the later migrations as unapplied (the output reads Unapplying catalog.0005_rename_author_name…​ FAKED), leaving the schema ahead of the recorded state. Use it deliberately.

The MIGRATION_MODULES setting overrides the package that holds an app’s migrations, which is how you keep generated migrations for a third-party app in your own project (or disable them in tests with None, a practice that hides real problems and is best avoided). makemigrations and migrate also gained a Command.autodetector attribute in Django 5.2, so a custom command can plug in a subclass of the autodetector.

Pitfalls

  • Editing an applied migration. Never change a migration that has run anywhere else; add a new one. The exception is fixing a data migration that imported a real model instead of using apps.get_model(), which Django’s guide explicitly allows.

  • Importing the real model in RunPython. Works today, fails when a new installation replays old migrations. Use apps.get_model().

  • Relying on custom methods in migrations. Historical models have no custom save(), constructors or methods: the logic you need has to live in the migration.

  • Answering the autodetector wrongly. --noinput answers "no" to the rename question, so a rename to a nullable field becomes drop plus add and the data is lost. Read the plan, run sqlmigrate, review the file.

  • One migration for everything. Mixing schema changes and data changes (especially on PostgreSQL) causes pending-trigger errors and long locks. Separate them.

  • Long transactions. A big backfill in an atomic migration holds locks for its duration. Use atomic = False and batches.

  • Dropping a column that running code reads. Deploy the code that stops using it first, then drop it.

  • Merge conflicts resolved by deleting a migration. Never delete a migration that has been applied anywhere; merge or re-point dependencies.

  • Squashing without porting functions. RunPython references become invalid Python; copy the functions over.

  • Dependent views and triggers. They are invisible to the ORM and break table rebuilds (SQLite) and drops (PostgreSQL since Django 6.0 no longer cascades).

  • SQLite as production. The emulated ALTER TABLE is slow and fragile; test migrations on the production engine.

  • Running migrate from every replica. Run it once per deploy from a release job.

  • Forgetting makemigrations --check in CI. A forgotten migration then reaches production as a mismatch between code and schema.

  • Using --fake to "fix" errors. It records something that did not happen. Find the cause instead.

Where the books are outdated

The books that accompany this section predate several changes in how migrations work and look:

  • Older material describes South and syncdb. South was replaced by built-in migrations in Django 1.7 and syncdb was removed in Django 1.9, so every example here uses makemigrations and migrate.

  • Initial migrations in older books show AutoField ids. DEFAULT_AUTO_FIELD was added in Django 3.2 and defaults to BigAutoField since Django 6.0, so new migrations create BigAutoField (as in the generated file above) unless the app configures otherwise.

  • Old migrations that mention NullBooleanField (deprecated in 3.1, removed in 4.0), index_together (deprecated in 4.2, removed in 5.1) or AlterIndexTogether need editing or squashing. Since 4.2 the advice is to use Meta.indexes (whose migrations are AddIndex and, since 4.1, RenameIndex) and UniqueConstraint instead of unique_together.

  • Material written before Django 3.1 and 4.x does not mention migrate --check, migrate --prune, makemigrations --scriptable and --update, optimizemigration, or the symbols in the makemigrations summary.

What changed recently

Table 7. Migration-related changes by version
Version Change

3.1

migrate --check exits non-zero when unapplied migrations exist.

4.1

optimizemigration, migrate --prune, makemigrations --scriptable, and RenameIndex (with autodetection when you rename Meta.indexes entries). makemigrations --noinput logs its default answers and why it could not create migrations. Python files created by these commands are formatted with black if it is on the PATH.

4.2

makemigrations --update. makemigrations --check no longer creates missing migration files. AlterIndexTogether is supported only for pre-4.2 migration files, and enum.Flag serialization.

5.0

Serialization of functools.cache() and functools.lru_cache() decorated functions.

5.1

Operation.category and the makemigrations operation symbols.

5.2

AlterConstraint (changes a constraint without dropping and recreating it); makemigrations and migrate have a Command.autodetector attribute.

6.0

Squashed migrations can be squashed again before being transitioned to normal migrations; ZoneInfo serialization; RemoveField no longer uses CASCADE on PostgreSQL (and the base schema editor); DEFAULT_AUTO_FIELD defaults to BigAutoField; the PostgreSQL extension operations accept hints.

6.1

Renaming a model through a migration also renames its permissions' name and codename. Django 6.1 requires PostgreSQL 15+, MySQL 8.4+, MariaDB 10.11+ and SQLite 3.37+.

Source: the release notes of 3.1, 4.1, 4.2, 5.0, 5.1, 5.2, 6.0 and 6.1.