Run and Inspect Migrations

Run and inspect migrations — Django Web Development.

Focus: run and inspect migrations

Sponsored

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.
  • makemigrations creates the instructions based on differences between your models and the current migration history.
  • migrate applies those instructions to the actual database.
  • sqlmigrate previews the raw SQL without executing it.
  • showmigrations lists 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 diff before committing them. A quick git diff shows you exactly what changed, and python manage.py showmigrations --plan gives 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 --plan is 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-run and migrate --plan before you commit and deploy. Use sqlmigrate when you need to understand the exact SQL. Use showmigrations whenever 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 with migrate.
  • You can use migrate --plan, sqlmigrate, and showmigrations to 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 migrate without 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 --fake as a universal fix for migration issues — it marks migrations as applied without executing them, which can mask real schema mismatches.
  • Ignoring the django_migrations table as a black box — understanding it helps debug out-of-sync states.
  • Not using migrate --plan before deploying to production, missing critical schema changes.

Variations

  1. Using makemigrations --check in CI pipelines to catch model changes that haven't been migrated yet.
  2. Using migrate --fake-initial to synchronize an existing database with migrations without re-running the initial schema.
  3. 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 --plan before applying schema changes to avoid downtime.
  • Team collaboration: using makemigrations --merge to resolve conflicting migration branches from parallel work.
  • Database auditing: using sqlmigrate to 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 --plan to preview what will be applied, and showmigrations to verify the current state.
  • The django_migrations table 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.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.