What a Migration Is (and What It Is Not)
A Django migration is a versioned, executable description of how to change your database schema (and sometimes your data) to match changes in your models. When you edit models.py, nothing happens to the database until you create and apply migrations.
- Models describe the desired state.
- Migrations are the steps to move from one state to the next.
- The database is the current state that must be evolved safely.
Django stores which migrations have been applied in a table called django_migrations. This is how it knows what to run next.
Why migrations matter for safe evolution
In real projects, your schema changes continuously: new fields, renamed columns, relationship changes, constraints, and data backfills. Migrations let you apply these changes in a controlled, repeatable way across environments (local, staging, production) and across teammates.
Core Commands You Will Use
| Task | Command | Notes |
|---|---|---|
| Create migrations from model changes | python manage.py makemigrations | Generates new migration files per app. |
| Apply migrations | python manage.py migrate | Executes unapplied migrations in dependency order. |
| See migration plan | python manage.py migrate --plan | Shows what would run, without running it. |
| Inspect SQL | python manage.py sqlmigrate app_label 000X | Shows SQL Django will execute for that migration. |
| List migration status | python manage.py showmigrations | Shows applied/unapplied migrations per app. |
Practical: create, inspect, apply
Assume you changed a model in an app named billing.
# 1) Create migration files based on model diffs
python manage.py makemigrations billing
# 2) Inspect what Django generated
python manage.py showmigrations billing
python manage.py sqlmigrate billing 0003
# 3) Apply to the database
python manage.py migrate
# 4) Confirm plan is empty (everything applied)
python manage.py migrate --planHow Django Decides What to Migrate
When you run makemigrations, Django compares your current model state to the last recorded state in the latest migration file for that app. It then generates operations such as AddField, AlterField, RenameField, CreateModel, DeleteModel, AddIndex, etc.
Continue in our app.
You can listen to the audiobook with the screen off, receive a free certificate for this course, and also have access to 5,000 other free online courses.
Or continue reading below...Download the app
Migration files live in your_app/migrations/ and are committed to version control. They are part of your codebase, not generated artifacts to ignore.
Migration Dependencies and Ordering
Each migration declares dependencies, typically on the previous migration in the same app, and sometimes on migrations in other apps (for example, when you add a foreign key to a model in another app). Django builds a dependency graph and applies migrations in a topological order.
Inspecting dependencies
Open a migration file and look for dependencies:
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("billing", "0002_auto_20260116_1012"),
("accounts", "0005_userprofile"),
]
operations = [
migrations.AddField(...),
]If two developers create migrations off the same base migration, you can end up with two “leaf” migrations in one app. Django will ask you to merge them.
Handling multiple leaf nodes (merge migrations)
# Detect conflicts
python manage.py makemigrations
# If Django reports multiple leaf nodes, create a merge migration
python manage.py makemigrations --mergeA merge migration typically has no operations; it just declares dependencies on both conflicting migrations so the graph becomes linear again.
Typical Change Scenarios (and Safe Patterns)
1) Adding a non-nullable field to a table with existing rows
This is one of the most common pitfalls. If a table already has rows, adding a NOT NULL column requires a value for existing rows.
Option A: Provide a one-off default during migration
When you add a field like status with null=False and no default, Django will prompt for a one-off default to populate existing rows.
# models.py (example)
class Invoice(models.Model):
# ... existing fields ...
status = models.CharField(max_length=20)Then:
python manage.py makemigrations billingDjango will ask for a default for existing rows. This default is baked into the migration operation, not necessarily into your model field definition.
Option B (recommended for production): Two-step migration to avoid long locks
For large tables, a safer pattern is:
- Step 1: Add the field as nullable (or with a database default), deploy.
- Step 2: Backfill data in a data migration, deploy.
- Step 3: Alter the field to non-nullable, deploy.
# Step 1: add nullable
status = models.CharField(max_length=20, null=True, blank=True)python manage.py makemigrations billing
python manage.py migrateThen create a data migration to fill status for existing rows (shown later), and finally:
# Step 3: enforce NOT NULL
status = models.CharField(max_length=20)python manage.py makemigrations billing
python manage.py migrateThis staged approach reduces risk because schema change and data backfill are separated and reversible.
2) Renaming a field (preserve data)
If you simply change the attribute name in the model, Django might interpret it as “remove old field, add new field,” which would drop the old column and create a new one (data loss). Use an explicit rename migration operation.
Step-by-step safe rename
- Change the model field name.
- Run
makemigrationsand ensure Django generatesRenameField. - If it does not, edit the migration to use
RenameField(or create an empty migration and add it).
# Example migration operation
migrations.RenameField(
model_name="invoice",
old_name="total",
new_name="total_amount",
)Verify SQL before applying:
python manage.py sqlmigrate billing 0004On most databases this becomes an ALTER TABLE ... RENAME COLUMN ... which preserves data.
3) Altering relationships (ForeignKey / ManyToMany)
Relationship changes can affect constraints, indexes, and join tables. Common cases include changing on_delete, making a foreign key optional, or pointing to a different model.
Making a ForeignKey optional (nullable)
# Before
customer = models.ForeignKey(Customer, on_delete=models.PROTECT)
# After
customer = models.ForeignKey(Customer, on_delete=models.PROTECT, null=True, blank=True)python manage.py makemigrations
python manage.py sqlmigrate billing 0005
python manage.py migrateThis typically results in an ALTER TABLE to drop the NOT NULL constraint.
Changing a ManyToMany relationship
Many-to-many fields are backed by an intermediate table. If you change the through model or rename the field, Django may need to create a new table and migrate data. Plan this carefully and consider a custom migration that copies rows from the old join table to the new one.
Pointing a ForeignKey to a different model
This is a high-impact change because existing rows may reference IDs that do not exist in the new target table. A safe approach is often:
- Add a new foreign key field pointing to the new model (nullable).
- Backfill it using a data migration (mapping old references to new ones).
- Update application code to use the new field.
- Remove the old foreign key field later.
Data Migrations: Updating Existing Rows Safely
Schema migrations change tables/columns. Data migrations change the data itself. Django supports data migrations via RunPython (and RunSQL when needed).
Creating a data migration
Create an empty migration and add a RunPython operation:
python manage.py makemigrations billing --empty --name backfill_invoice_statusEdit the generated file:
from django.db import migrations
def forwards(apps, schema_editor):
Invoice = apps.get_model("billing", "Invoice")
# Example: set status for rows where it is NULL
Invoice.objects.filter(status__isnull=True).update(status="draft")
def backwards(apps, schema_editor):
Invoice = apps.get_model("billing", "Invoice")
# Reverse strategy depends on your needs; here we undo only what we set
Invoice.objects.filter(status="draft").update(status=None)
class Migration(migrations.Migration):
dependencies = [
("billing", "0003_add_status_nullable"),
]
operations = [
migrations.RunPython(forwards, backwards),
]Why apps.get_model matters
In migrations, you should not import your models directly from billing.models. Migrations must run against the historical version of the model at that point in the migration graph. apps.get_model() gives you the correct historical model state.
When to use RunSQL
Use RunSQL when you need database-specific features or performance (for example, complex updates on large tables). Keep it as portable as possible and provide a reverse SQL statement when feasible.
migrations.RunSQL(
sql="UPDATE billing_invoice SET status = 'draft' WHERE status IS NULL;",
reverse_sql="UPDATE billing_invoice SET status = NULL WHERE status = 'draft';",
)Inspecting and Reasoning About Migration Files
Read operations like a checklist
A migration file is a list of operations. Before applying in shared environments, scan for:
- Destructive operations:
RemoveField,DeleteModel - Potentially expensive operations: adding non-null columns, adding indexes on large tables
- Ambiguous changes: field rename interpreted as drop/add
Use sqlmigrate to validate impact
Always inspect SQL for risky changes:
python manage.py sqlmigrate billing 0006This helps you catch unintended drops, constraint rebuilds, or table rewrites before they happen.
Working Across Environments (Local, CI, Staging, Production)
Recommended workflow
- Local development: change models, run
makemigrations, runmigrate, run tests. - Commit: commit both code changes and migration files together.
- CI: run
python manage.py migrate --noinputand tests on a fresh database to ensure migrations apply cleanly. - Staging: apply migrations first, then deploy application code if your process separates them; verify key flows.
- Production: apply migrations with a plan for rollback and with attention to locking/long-running operations.
Practical checklist before shipping a migration
- Run
python manage.py makemigrationsand ensure there are no uncommitted model changes. - Run
python manage.py migrate --planto see what will execute. - Run
python manage.py sqlmigratefor any migration that alters large tables or constraints. - Ensure data migrations are idempotent where possible (safe to re-run or safe if partially applied).
- Confirm reversibility: provide
backwardsforRunPythonwhen feasible.
Collaboration: Avoiding Migration Conflicts
Rules of thumb for teams
- Commit migrations early with the model change that requires them.
- Pull often before creating migrations to reduce divergence.
- One feature branch, consistent migration history: if your branch spans many commits, consider squashing migrations before merge (only if not yet deployed anywhere shared).
- Never edit migrations that have been applied in shared environments (staging/production). Create a new migration to correct course.
Dealing with conflicting migrations
Scenario: two developers both create 0007_... from 0006. When merged, Django sees two leaf nodes.
# Create a merge migration that depends on both leaves
python manage.py makemigrations --mergeThen apply normally:
python manage.py migrateWhen an “empty” migration is useful
Empty migrations are helpful when you need to control the order of operations or add custom steps (data backfill, constraint changes) that Django cannot infer from model diffs.
python manage.py makemigrations billing --empty --name add_constraint_in_stepsAdvanced Safety Notes You Should Know
Atomic migrations
By default, Django runs migrations in a transaction on databases that support DDL transactions. Some operations (especially on certain databases) cannot run inside a transaction. You can control this with atomic = False in a migration class, but do so intentionally because it changes rollback behavior.
class Migration(migrations.Migration):
atomic = False
dependencies = [...]
operations = [...]Separating schema and data changes for deploy safety
A common production-safe pattern is to deploy in phases:
- Phase 1: Add new nullable columns / new tables; keep old code working.
- Phase 2: Backfill data (migration or background job), start writing to both old and new fields if needed.
- Phase 3: Switch reads to new fields; remove old fields later.
This reduces downtime risk and makes rollbacks easier.
Faking migrations (use with caution)
--fake and --fake-initial can mark migrations as applied without running them. This is useful when the database schema already matches (for example, integrating an existing database), but it can also hide real drift if used incorrectly.
# Mark as applied without executing SQL
python manage.py migrate billing 0008 --fake