Run and Inspect Migrations
Run and inspect migrations — Django Web Development.
Focus: run and inspect migrations
You've built your models, wired them into the admin, and maybe even run makemigrations a few times. But do you really know what those migration files contain, and what actually happens when you run migrate? If you're nodding along but secretly hoping you never have to open a migration file, this lesson is for you. We'll demystify the life cycle of a Django migration — from model definition to database schema — and arm you with the commands and mental habits to inspect, verify, and run migrations with total confidence. By the end, you'll not only run migrations without fear, but you'll also be able to explain what Django is doing under the hood (and spot problems before they blow up in production).
The problem this lesson solves
Imagine this: you've just added a new author field to your BlogPost model. You run makemigrations and it says "Migrations for 'blog': 0002_blogpost_author.py". You breathe a sigh of relief. Then you run migrate and it applies. Done, right? Not so fast.
What if that migration had a subtle bug? What if it tried to add a non-null field to a table with existing rows, and the migration failed halfway through, leaving your database in an inconsistent state? What if you're working on a team and someone else's migration conflicts with yours? Or what if you need to roll back a migration but have no idea which ones have been applied?
The problem is that many developers treat migrations as a black box. They run the magic commands and hope for the best. But in production, a botched migration can mean downtime, data loss, or a frantic late-night rollback. Understanding how to run and inspect migrations — not just execute them blindly — is the difference between being a Django user and a Django professional. This lesson gives you the exact tools and mindset to inspect what Django will do before it touches your data, and to troubleshoot confidently when things go wrong.
Core concept / mental model
Think of Django migrations as a version control system for your database schema, analogous to how Git versions your code. Each migration file is a commit: it records a set of changes (add a table, add a field, alter an index) that, when applied, transform your database schema from one state to the next.
- Models are the source of truth — they define what your data should look like.
- Migrations are the instructions that tell Django how to make the database match those models.
makemigrationscreates the instructions based on differences between your models and the current migration history.migrateapplies those instructions to the actual database.sqlmigratepreviews the raw SQL without executing it.showmigrationslists which migrations have been applied and which haven't.
A helpful analogy: your models are the blueprint for a house; migrations are the step-by-step work orders for the construction crew. You wouldn't build a house without reviewing the work orders — and you shouldn't alter your database without reviewing the migrations.
Django keeps track of applied migrations in a special table called django_migrations, which stores the app name, migration name, and timestamp. This is how Django knows which migrations to apply next — and why you should never delete migration files casually; you'd break the history chain.
How it works step by step
When you run a migration-related command, Django executes a predictable sequence. Let's walk through the lifecycle from model change to applied schema.
Step 1: Detect model changes — When you run python manage.py makemigrations, Django compares your current models against the migration files in each app's migrations/ folder. It builds a list of operations (like CreateModel, AddField, RemoveField) needed to reconcile the two.
Step 2: Create migration files — Django writes a Python file (e.g., 0002_blogpost_author.py) to the app's migrations/ directory. This file contains a Migration class with a dependencies list (which migrations it depends on) and an operations list (exactly what to change).
Step 3: Review — You should always inspect the generated migration before applying. Open the file and read it. Check that the operations make sense for your existing data. This is your chance to catch issues early.
Step 4: Plan the migration — Run python manage.py migrate --plan. This shows a list of migrations that would be applied, in order, without actually doing anything. It's a dry run that tells you 'here's what's going to happen'.
Step 5: Apply — Run python manage.py migrate. Django reads the migration files that haven't been applied, executes their operations in order, and records them in the django_migrations table. If any step fails, Django rolls back the entire migration (within a transaction if the database supports it) so you don't end up with a half-applied schema.
Step 6: Verify — Use python manage.py showmigrations to see a checklist of which migrations are applied ([X]) and which are not ([ ]). You can also use sqlmigrate to see the exact SQL that was run, or inspect the database directly.
Pro tip: Always review your migration files with
git diffbefore committing them. A quickgit diffshows you exactly what changed, andpython manage.py showmigrations --plangives a similar view across all apps. Make it a habit before every deploy.
Hands-on walkthrough
Let's put this into practice. We'll start with an existing Django project (if you've been following the track, you can use the one from previous lessons). If not, create a minimal project with a blog app and a BlogPost model.
First, let's inspect the current state of migrations:
python manage.py showmigrations
Expected output (truncated for brevity):
admin
[X] 0001_initial
auth
[X] 0001_initial
[X] 0002_alter_user_first_name_max_length
...
blog
[X] 0001_initial
sessions
[X] 0001_initial
The [X] means the migration has been applied. Good.
Now, let's add a new field to the BlogPost model. Open models.py in your blog app and add:
class BlogPost(models.Model):
title = models.CharField(max_length=100)
content = models.TextField()
# ... other fields
author = models.CharField(max_length=50, default='anonymous')
Now generate the migration:
python manage.py makemigrations
Expected output:
Migrations for 'blog':
blog/migrations/0002_blogpost_author.py
- Add field author to blogpost
Now — the key step: inspect the migration file. Open blog/migrations/0002_blogpost_author.py.
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('blog', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='blogpost',
name='author',
field=models.CharField(default='anonymous', max_length=50),
),
]
Notice the dependencies list — this migration depends on 0001_initial. The operations list contains a single AddField operation. Because we provided a default, this migration will work even if there are existing rows in the table.
Before applying, let's see the raw SQL that will run:
python manage.py sqlmigrate blog 0002
Expected output (SQLite dialect shown; may vary by database):
BEGIN;
-- Add field author to blogpost
ALTER TABLE "blog_blogpost" ADD COLUMN "author" varchar(50) NOT NULL DEFAULT 'anonymous';
COMMIT;
Now apply it:
python manage.py migrate
Expected output:
Operations to perform:
Apply all migrations: admin, auth, blog, contenttypes, sessions
Running migrations:
Applying blog.0002_blogpost_author... OK
Finally, verify with showmigrations:
python manage.py showmigrations
Output now includes:
blog
[X] 0001_initial
[X] 0002_blogpost_author
Pro tip:
migrate --planis your best friend before deploying. It shows you the order and the number of pending migrations — and you can even use it in CI to detect out-of-sync schemas.
Compare options / when to choose what
You now have several commands — each serves a distinct purpose. Here's a quick comparison to help you decide which to use when.
| Command | What it does | When to use it |
|---|---|---|
makemigrations |
Creates migration files based on model changes | When you've changed models and need to save those changes as a migration |
migrate |
Applies unapplied migrations to the database | To change the database schema (in dev, CI, or production) |
migrate --plan |
Shows the list of migrations that would be applied | Before deploying, to review what's about to happen |
sqlmigrate |
Shows the raw SQL for a migration without running it | To verify exactly what SQL will be executed (auditing, or for DBAs) |
showmigrations |
Lists migrations and their applied state | To check the current status, or to debug why a migration isn't applied |
makemigrations --dry-run |
Shows what would be created without writing files | To preview new migrations before generating them |
Rule of thumb: Use
makemigrations --dry-runandmigrate --planbefore you commit and deploy. Usesqlmigratewhen you need to understand the exact SQL. Useshowmigrationswhenever something feels out of sync.
Variations: You might also encounter makemigrations --check, which exits with a non-zero status if there are model changes pending — perfect for CI pipelines. Also, migrate <app_label> applies migrations only for a specific app, which can be useful when apps have cross-dependencies.
Troubleshooting & edge cases
1. "Migration ... is applied; its dependencies not applied"
This happens when the django_migrations table records a migration as applied, but its dependencies aren't. Often caused by manually editing the table or by a partially deployed migration. The safest fix is to use migrate --fake to mark the migration as applied without running it, but only if you're sure the schema changes are already present in the database. For example:
python manage.py migrate --fake
However, use this with extreme caution — --fake can mask real problems.
2. Non-nullable field added with no default, and existing rows
If you add a field like author = models.CharField(max_length=50) without a default, and the table has existing rows, the migration will fail with an IntegrityError because the new column can't be empty. The solution is to provide a default or null=True, or to create a data migration to populate existing rows. Always inspect your migration for this scenario.
3. Migration conflicts with another developer's work
You may see an error like:
Conflicting migrations detected; multiple leaf nodes in the migration graph
This means two branches of migration history exist. The fix is to merge them by creating a migration that depends on both branches. Use makemigrations --merge to generate a merge migration.
4. "No changes detected" when you expect changes
This usually means your model changes are already represented in the migrations, or you're looking at the wrong app. Run makemigrations --dry-run --verbosity 3 to see detailed output.
5. Migration files out of sync with the database
If you've manually edited the database or deleted migration files, showmigrations may not reflect reality. Restore the missing migration files or use --fake to mark migrations as applied (only if the schema truly matches).
Pro tip: Before any migration, back up your database. In production, consider running migrations as part of a deployment script that can roll back automatically if something fails.
What you learned & what's next
You've now mastered the core of Django's migration system. Let's recap what you achieved:
- You understand that migrations are version control for your database, with models as the source of truth.
- You can run
makemigrations, inspect the generated files, and apply them withmigrate. - You can use
migrate --plan,sqlmigrate, andshowmigrationsto inspect and verify migration state. - You know how to troubleshoot common migration pitfalls, from non-null field errors to conflicting migration branches.
The ability to confidently run and inspect migrations is a foundational skill for any Django developer. You're not just executing commands — you're deliberately managing your database schema evolution.
Now that you're comfortable with migrations, the next lesson in this track will explore how to seed your database with initial data using fixtures or data migrations. You'll learn how to populate your tables with meaningful test data — a crucial step for building features and writing tests.
Go ahead and try it out: add another field to your model, run makemigrations --dry-run to preview, then apply it. Build that instinct to inspect before you act. Your future self (and your production database) will thank you.
Practice recap
Now, put it into practice: add a new field to a model of your own project, generate the migration, inspect the SQL with sqlmigrate, and apply it with migrate. Then check showmigrations to confirm. This hands-on exercise will solidify your understanding of the entire migration lifecycle.
Common mistakes
- Running
migratewithout inspecting the generated migration file — dangerous when adding non-null fields to tables with existing rows. - Deleting migration files thinking they're no longer needed — breaks the migration history and causes 'inconsistent migration history' errors.
- Using
--fakeas a universal fix for migration issues — it marks migrations as applied without executing them, which can mask real schema mismatches. - Ignoring the
django_migrationstable as a black box — understanding it helps debug out-of-sync states. - Not using
migrate --planbefore deploying to production, missing critical schema changes.
Variations
- Using
makemigrations --checkin CI pipelines to catch model changes that haven't been migrated yet. - Using
migrate --fake-initialto synchronize an existing database with migrations without re-running the initial schema. - Using data migrations (via
RunPython) alongside schema migrations when you need to backfill or transform data during a schema change.
Real-world use cases
- Production deployment: running
migrate --planbefore applying schema changes to avoid downtime. - Team collaboration: using
makemigrations --mergeto resolve conflicting migration branches from parallel work. - Database auditing: using
sqlmigrateto review and archive the exact SQL executed for a migration.
Key takeaways
- Migrations are version control for your database — each migration file records specific schema operations.
- Always inspect generated migration files before applying them; catch non-null field issues early.
- Use
migrate --planto preview what will be applied, andshowmigrationsto verify the current state. - The
django_migrationstable tracks applied migrations, and should remain in sync with your database schema. - For non-null fields on existing tables, provide a default or use a data migration.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.