PYTHON / DJANGO
Migrations
Understand how Django builds migrations by diffing rebuilt model state against models.py, and apply, inspect, and reverse them safely.
What you will learn
- Explain why makemigrations diffs migration history against models.py, not the database
- Read a generated migration's dependencies and operations list
- Use apps.get_model inside RunPython so data migrations see historical models
- Preview generated SQL with sqlmigrate before running migrate
Understanding Migrations
When you run makemigrations, Django never looks at your database. It loads every migration file belonging to your installed apps, replays their operations in memory to rebuild a ProjectState (a description of what the models looked like after the last migration), then compares that state field by field against the models currently defined in your Python files. The differences become operations such as AddField, AlterField, or RemoveField, written into a new numbered file. This is why the migration files, not the database, are the authoritative record of what the schema should be.
migrate is the other half. It reads the django_migrations table to see which (app_label, name) pairs have already run, orders the remaining ones using each migration's dependencies list, and hands each operation to a schema editor that emits real DDL. On PostgreSQL and SQLite each migration runs inside a transaction, so a failing operation rolls back; MySQL has no transactional DDL, so a half-applied migration can leave the schema partly changed. Once a name is recorded in django_migrations, Django will never run that file again, which is exactly why editing an already-applied migration has no effect on machines that already applied it.
Schema changes and data changes are different concerns. AddField can create a column, but only RunPython can fill it in, and inside a RunPython function you must call apps.get_model('shop', 'Product') rather than importing Product directly. The imported class reflects today's models.py, which may have fields that do not exist yet at this point in history; apps.get_model hands you a model rebuilt from the state at that migration, so the function keeps working years later. Pass a second callable (or migrations.RunPython.noop) if you want the migration to be reversible.
import django
from django.conf import settings
settings.configure(INSTALLED_APPS=[])
django.setup()
from django.db import models
from django.db.migrations.autodetector import MigrationAutodetector
from django.db.migrations.graph import MigrationGraph
from django.db.migrations.questioner import NonInteractiveMigrationQuestioner
from django.db.migrations.state import ModelState, ProjectState
# State rebuilt from the existing migration files.
before = ProjectState()
before.add_model(ModelState("shop", "Product", [
("id", models.AutoField(primary_key=True)),
("name", models.CharField(max_length=100)),
]))
# State described by the current models.py.
after = ProjectState()
after.add_model(ModelState("shop", "Product", [
("id", models.AutoField(primary_key=True)),
("name", models.CharField(max_length=200)),
("price", models.DecimalField(max_digits=6, decimal_places=2, default=0)),
]))
detector = MigrationAutodetector(before, after, NonInteractiveMigrationQuestioner())
changes = detector.changes(graph=MigrationGraph(), convert_apps={"shop"})
for migration in changes["shop"]:
for operation in migration.operations:
print(operation.describe())
A migration is a recorded diff between the model state rebuilt from earlier migration files and your current models.py, not a snapshot of the live database.
Worked examples
Dependency order decides the apply order
Shows how Django turns a migration's dependencies into the plan migrate actually executes.
import django
from django.conf import settings
settings.configure(INSTALLED_APPS=[])
django.setup()
from django.db.migrations.graph import MigrationGraph
graph = MigrationGraph()
for key in [("shop", "0001_initial"), ("shop", "0002_price"), ("orders", "0001_initial")]:
graph.add_node(key, None)
graph.add_dependency(None, ("shop", "0002_price"), ("shop", "0001_initial"))
graph.add_dependency(None, ("orders", "0001_initial"), ("shop", "0002_price"))
for node in graph.forwards_plan(("orders", "0001_initial")):
print(node)
Example explained
Line 1add_node registers one migration file; the second argument is the loaded Migration object, unused here.
Line 2add_dependency(None, child, parent) is what a dependencies entry in a migration file becomes internally.
Line 3forwards_plan walks parents first, so orders.0001 cannot run before the shop table its ForeignKey points at exists.
Line 4The plan is per-target, not global: asking for shop.0002_price would return only the two shop nodes.
Why RunPython needs a reverse function
Demonstrates that a data migration is only reversible if you supply reverse code.
import django
from django.conf import settings
settings.configure(INSTALLED_APPS=[])
django.setup()
from django.db import migrations
def fill_slugs(apps, schema_editor):
Product = apps.get_model("shop", "Product")
for product in Product.objects.all():
product.slug = product.name.lower().replace(" ", "-")
product.save(update_fields=["slug"])
forward_only = migrations.RunPython(fill_slugs)
reversible = migrations.RunPython(fill_slugs, migrations.RunPython.noop)
print(forward_only.reversible)
print(reversible.reversible)
print(reversible.describe())
Example explained
Line 1apps.get_model returns the historical Product, so this code keeps working after later migrations change the model.
Line 2reversible is simply reverse_code is not None; without it, migrate shop 0001 refuses to unapply this migration.
Line 3RunPython.noop is a do-nothing callable used when undoing the data change needs no work.
Line 4update_fields limits the UPDATE to the slug column, which matters when the migration touches many rows.
Important notes
makemigrations touches no database and needs no connection; only migrate and sqlmigrate care which backend you use.
Deleting migration files to "start clean" only works if you also clear the matching rows in django_migrations and the tables themselves, which means losing data.
Common mistakes
Editing or renaming a migration that has already been applied: django_migrations still lists the old name as applied, so the change never runs on that machine and the schema quietly drifts from the models.
Answering the one-off default prompt with something like timezone.now(): the value is evaluated once and hardcoded into the migration file, so every existing row ends up with the identical timestamp.
Answering "no" to "Did you rename product.title to product.name?": Django writes RemoveField plus AddField instead of RenameField, and applying it drops the old column along with its data.
Try it yourself
Change, predict, then run
Take the main example and change the after state so name is removed and a sku = models.CharField(max_length=20, unique=True, default="") is added, then rerun it and note which operations appear and in which order.
Open the Python workspaceCheck your understanding
You clone a colleague's project and their models.py has a field that no migration file mentions. What does python manage.py migrate do about that field?
- Nothing: migrate only replays migration files, so the column is never created and queries using that field fail at runtime.
- It creates the column, because migrate synchronises the database to match models.py.
- It refuses to start and reports that models.py and the migration files disagree.
- It creates the column but leaves django_migrations unchanged, so the next migrate recreates it.
Show answer
migrate executes the operations stored in migration files and records their names; models.py is only read by makemigrations. Option 2 describes the old pre-1.7 syncdb behaviour of creating tables straight from model definitions, which Django no longer does, and nothing in migrate compares models.py against the schema, so no error is raised either.