Skip to content

Django Migrations — Complete Guide

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Django Migrations. We cover key concepts, practical examples, and best practices to help you master this topic.

Learn Django migrations: auto-detect model changes, generate migrations, apply and rollback, handle data migrations, manage migration conflicts, and best practices for Django schema evolution.

What You Learn

You will learn how to use Django's built-in migration system: auto-detect model changes with makemigrations, apply migrations with migrate, write data migrations, handle squashing and merging, and follow Django-specific best practices.

Why It Matters

Django has the most developer-friendly migration system. It auto-detects model changes and generates migrations automatically. You rarely write SQL. Understanding Django migrations is essential for any Django developer.

Real-World Use

DodaTech's Django application has 15 apps with 847 total migrations. The auto-detection feature generates 95% of migrations automatically. Data migrations handle complex backfilling. Squashing reduces migration count for faster test database creation.

Generating Migrations

# Auto-detect model changes and create migrations
python manage.py makemigrations

# Output:
# Migrations for 'users':
#   users/migrations/0003_user_phone.py
#     - Add field phone to user

# Preview SQL without applying
python manage.py sqlmigrate users 0003

# Output:
# BEGIN;
# ALTER TABLE "users_user" ADD COLUMN "phone" VARCHAR(20) NULL;
# COMMIT;
# models.py - Model change that triggers migration
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)
    phone = models.CharField(max_length=20, blank=True, null=True)  # New field
    is_active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

Expected output: makemigrations scans all models for changes and generates migration files. sqlmigrate shows the SQL without executing. Migration files are stored per-app in migrations/ directories.

Applying and Rolling Back

# Apply all pending migrations
python manage.py migrate

# Apply specific app migration
python manage.py migrate users 0003

# Rollback to a specific migration
python manage.py migrate users 0002

# Rollback all migrations for an app
python manage.py migrate users zero

# List migrations and their status
python manage.py showmigrations

# Output:
# users
#  [X] 0001_initial
#  [X] 0002_add_bio
#  [ ] 0003_user_phone

Expected output: migrate applies pending migrations. Specify app and migration number for rollback. showmigrations lists all migrations with applied status ([X] = applied, [ ] = pending).

Migration File Structure

# users/migrations/0003_user_phone.py
from django.db import migrations, models

class Migration(migrations.Migration):
    dependencies = [
        ('users', '0002_add_bio'),
    ]

    operations = [
        migrations.AddField(
            model_name='user',
            name='phone',
            field=models.CharField(blank=True, max_length=20, null=True),
        ),
    ]

Expected output: Django migration file declares dependencies on previous migrations and lists operations. Operations are ORM-aware (model_name, field references). No raw SQL unless explicitly used.

Data Migrations

# Create an empty migration for data operations
python manage.py makemigrations users --empty --name backfill_full_name

# Creates: users/migrations/0004_backfill_full_name.py
# users/migrations/0004_backfill_full_name.py
from django.db import migrations

def backfill_full_name(apps, schema_editor):
    User = apps.get_model('users', 'User')
    for user in User.objects.filter(full_name__isnull=True):
        user.full_name = f'{user.first_name} {user.last_name}'.strip()
        user.save(update_fields=['full_name'])

def reverse_backfill(apps, schema_editor):
    User = apps.get_model('users', 'User')
    User.objects.all().update(full_name=None)

class Migration(migrations.Migration):
    dependencies = [
        ('users', '0003_user_phone'),
    ]

    operations = [
        migrations.RunPython(backfill_full_name, reverse_backfill),
    ]

Expected output: Data migrations use RunPython with forward and reverse functions. The apps parameter provides historical model versions (matching the migration's schema state). Use update_fields to avoid unnecessary updates.

Complex Schema Operations

# migrations/0005_complex_changes.py
from django.db import migrations, models

def migrate_data(apps, schema_editor):
    Order = apps.get_model('orders', 'Order')
    # Transform data before schema change
    for order in Order.objects.filter(status='new'):
        order.status = 'pending'
        order.save()

class Migration(migrations.Migration):
    dependencies = [
        ('orders', '0004_previous'),
    ]

    operations = [
        # Rename field
        migrations.RenameField(
            model_name='order',
            old_name='total',
            new_name='amount',
        ),

        # Change field type
        migrations.AlterField(
            model_name='order',
            name='status',
            field=models.CharField(
                choices=[
                    ('pending', 'Pending'),
                    ('paid', 'Paid'),
                    ('shipped', 'Shipped'),
                ],
                default='pending',
                max_length=20,
            ),
        ),

        # Add index
        migrations.AddIndex(
            model_name='order',
            index=models.Index(fields=['status'], name='idx_order_status'),
        ),

        # Data migration
        migrations.RunPython(migrate_data, migrations.RunPython.no_operation),
    ]

Expected output: Migration combines multiple operations. RenameField preserves data. AlterField changes column type. AddIndex creates indexes. RunPython handles data before or after schema changes.

Separating Apps

# Django manages migrations per app
# Each app has its own migrations/ directory

# app1 (users)
#   migrations/
#     0001_initial.py
#     0002_add_phone.py

# app2 (orders)
#   migrations/
#     0001_initial.py  # Depends on users.0001
#     0002_add_total.py

# Cross-app dependencies are tracked
# orders/migrations/0001_initial.py
from django.db import migrations, models

class Migration(migrations.Migration):
    initial = True

    dependencies = [
        ('users', '0001_initial'),  # Depends on users app
    ]

    operations = [
        migrations.CreateModel(
            name='Order',
            fields=[
                ('id', models.AutoField(primary_key=True)),
                ('user', models.ForeignKey(
                    to='users.User',
                    on_delete=models.CASCADE,
                )),
                ('total', models.DecimalField(max_digits=10, decimal_places=2)),
            ],
        ),
    ]

Expected output: Each Django app has its own migrations. Cross-app dependencies ensure correct ordering. The orders migration depends on users migration because Order references User.

Merging and Squashing

# Create a merge migration for conflicting branches
python manage.py makemigrations --merge

# Squash migrations (combine many into fewer)
python manage.py squashmigrations users 0005

# This creates users/migrations/0001_squashed_0005.py

# After squashing:
# - Delete the old migrations
# - Update dependencies in later migrations

Expected output: --merge creates a migration with multiple parent dependencies to resolve branching. squashmigrations combines migrations 0001-0005 into a single migration for faster test database creation.

Common Mistakes

1. Editing Applied Migrations

Editing a migration that has already been applied causes inconsistencies. Django detects the change and refuses to proceed. Create a new migration instead. Never modify applied migrations.

2. Forgetting --empty for Data Migrations

Running makemigrations without --empty creates a schema migration based on model changes. For data-only migrations, use --empty to create an empty migration then add RunPython.

3. Using Current Models in Data Migrations

Data migrations should use apps.get_model() not direct model imports. Direct imports use the current model state, not the historical state. apps.get_model() provides the model version matching the migration.

4. Long-Running Data Migrations Without Batching

Data migrations that update millions of rows lock tables and cause timeouts. Use Iterator() or batch processing. Commit periodically. Consider running large data migrations as management commands instead.

5. Not Testing Migrations

Migrations that work on an empty development database may fail on production data. Test migrations on a production copy. Test both forward and reverse migrations. Verify data integrity after migration.

Practice Questions

1. How does Django auto-detect model changes?

Django compares the current model definitions to the last migration state. It detects added/removed/changed fields, indexes, constraints, and meta options. It generates migration operations to match the current models.

2. What is the difference between makemigrations and migrate?

makemigrations creates migration files based on model changes. migrate applies pending migrations to the database. makemigrations is Code Generation. migrate is execution.

3. How do you write a data migration in Django?

Create an empty migration with --empty. Define functions that use apps.get_model() for historical models. Use RunPython with forward and reverse functions. The migration file contains the data transformation logic.

4. Why should data migrations use apps.get_model() instead of direct imports?

apps.get_model() provides the model version matching the migration's schema state. Direct imports use the current model, which may have additional fields or changes not yet applied. Historical models prevent schema mismatch errors.

Challenge

Set up Django migrations for a blog application with: User and Post models with migrations, data migration that generates slugs for existing posts, field rename migration (body to content), index addition on publish_date, cross-app dependency between apps, and migration squashing for the first 10 migrations.

FAQ

Can I manually edit Django migration files?

Yes, but it is risky. Manual edits may create inconsistencies between the migration and the current model state. If you must edit, test thoroughly. Prefer creating a new migration over editing existing ones.

How do I handle migration conflicts with multiple developers?

Run makemigrations after pulling changes. Use --merge if there are conflicting migration branches. Communicate schema changes in team standups. Review migrations in pull requests.

What happens if I delete a migration file that has been applied?

Django tracks applied migrations in the django_migrations table. If the file is deleted, Django cannot find it and migrate --list shows a question mark. Restore the file or create a replacement.

How do I reset migrations for an app?

Delete the migration files. Drop the app's tables. Run python manage.py makemigrations && python manage.py migrate. This recreates the initial migration. Only do this in development.

Can I use Django migrations with a non-Django database?

Yes. Django migrations can manage any database Django supports. The migration files are app-specific. Other applications should not modify tables managed by Django migrations.

Mini Project: Django Migration Workflow

Set up Django with: users and posts apps, initial migrations for both, a field change migration (add bio to User), a data migration (backfill post slugs), a complex migration (rename field, add index), migration squashing for the first 5 migrations, and CI/CD step that runs migrate before tests.

What's Next

Now that you understand Django migrations, explore Prisma Migrate for declarative schema migrations in TypeScript.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro