Understanding Django Migrations: How They Work, Commands, Workflow, and Backend Considerations

This article provides a comprehensive explanation of Django migrations—how they track changes to your models, how to create and apply them, the commands involved, backend-specific behaviors, and best practices for version control and team collaboration.

Django migrations, makemigrations, migrate, sqlmigrateshowmigrations, schema changesdatabase backends

~12 min read • Updated Mar 14, 2026

Introduction

Migrations are Django’s mechanism for propagating changes made to your models—such as adding fields, modifying options, or deleting models—into your database schema. They act as a version control system for your database structure, ensuring that schema changes are tracked, reproducible, and deployable across development, staging, and production environments.


The Migration Commands

Django provides several management commands to work with migrations:

  • migrate: Applies or unapplies migrations to the database.
  • makemigrations: Creates new migration files based on model changes.
  • sqlmigrate: Shows the SQL that a migration will run.
  • showmigrations: Lists all migrations and their applied status.

Think of makemigrations as creating “commits” for your schema, and migrate as applying those commits to the database.


Where Migrations Live

Each Django app contains a migrations/ directory where migration files are stored. These files should be committed to version control so that all environments apply the same schema changes consistently.

You can override the migration module location using the MIGRATION_MODULES setting if needed.


How Django Detects Changes

Django generates migrations for any change to your models—even changes that don’t affect the database schema. This ensures Django can reconstruct the full history of your models, which is important for data migrations and custom validation logic.


Backend Support and Caveats

PostgreSQL

PostgreSQL offers the strongest schema migration support. Most operations are transactional and safe.

MySQL

  • No transactional support for schema changes—failed migrations must be manually fixed.
  • MySQL 8.0 improves DDL performance but still may require table locks.
  • Index size limits may prevent certain indexes from being created.

SQLite

SQLite has limited schema alteration capabilities. Django emulates migrations by:

  1. Creating a new table
  2. Copying data
  3. Dropping the old table
  4. Renaming the new table

This works for development but is not recommended for production due to performance and reliability concerns.


Typical Migration Workflow

1. Modify your models

For example, add a field or delete a model.

2. Create migrations


$ python manage.py makemigrations

Django compares your models to existing migrations and generates new migration files. Always review the output to ensure Django detected your changes correctly.

3. Apply migrations


$ python manage.py migrate

This updates your database schema.

4. Commit your changes

Commit both the model changes and the migration files together so teammates and servers stay in sync.

Optional: Name your migration


$ python manage.py makemigrations --name changed_my_model your_app

Version Control and Conflicts

Since migrations are stored in version control, conflicts can occur when two developers create migrations with the same number. This is normal.

Django resolves this by using migration dependencies rather than file numbering. If Django detects two migrations that need ordering, it will prompt you to fix the conflict or attempt to linearize them automatically.

If manual intervention is needed, you can edit the migration files directly—this is safe and often simple.


Conclusion

Migrations are a powerful and essential part of Django’s architecture. They allow you to evolve your database schema safely and consistently across environments. By understanding the migration commands, workflow, backend limitations, and version control practices, you can manage schema changes confidently and avoid common pitfalls.

Transactions in Migrations

On databases that support DDL transactions—such as PostgreSQL and SQLite—Django runs each migration inside a single atomic transaction by default. On databases that do not support DDL transactions—such as MySQL and Oracle—migration operations run without transactional protection.

Disabling Transactions for a Migration


class Migration(migrations.Migration):
    atomic = False

You can also wrap specific operations inside atomic() or pass atomic=True to RunPython for partial transactional behavior.


Migration Dependencies

Although migrations are defined per app, Django must respect cross‑app relationships. For example, if you add a ForeignKey from Book to Author, the migration in books will depend on a migration in authors.

This ensures that the referenced table exists before Django attempts to create the foreign key constraint.

Important: Apps without migrations must not define relationships to apps that have migrations. This may appear to work temporarily but is unsupported.


Swappable Dependencies

The swappable_dependency() helper is used when a migration depends on a model that may be swapped out, such as Django’s user model:


swappable_dependency(settings.AUTH_USER_MODEL)

This ensures that migrations correctly reference the app containing the active implementation of the swappable model.


Migration Files

Migration files are Python modules containing a subclass of migrations.Migration. Django looks for two key attributes:

  • dependencies: migrations that must run first
  • operations: schema changes to apply

Example Migration File


class Migration(migrations.Migration):
    dependencies = [("migrations", "0001_initial")]

    operations = [
        migrations.DeleteModel("Tribble"),
        migrations.AddField("Author", "rating", models.IntegerField(default=0)),
    ]

Django uses these operations to build an in‑memory representation of your schema, compare it to your models, and generate SQL.

Although you rarely need to edit migration files manually, Django fully supports hand‑written migrations for complex operations.


Custom Fields and Migrations

You cannot change the number of positional arguments in a custom field once migrations referencing it exist. Old migrations will call the field’s constructor with the old signature, causing errors. If you need new arguments, add them as keyword arguments instead.


Model Managers in Migrations

To serialize a custom manager into migrations, set:


class MyManager(models.Manager):
    use_in_migrations = True

If using from_queryset(), subclass the generated manager to make it importable.


Initial Migrations

Initial migrations create the first version of an app’s tables. They are marked with:


initial = True

If not explicitly marked, Django treats the first migration in an app as initial.

Using --fake-initial

When running:


python manage.py migrate --fake-initial

Django checks whether the tables or columns already exist and marks the migration as applied if they do.


History Consistency

If you manually edit migration dependencies or merge branches incorrectly, you may create an inconsistent migration history—where a migration is applied but one of its dependencies is not.

Django refuses to run migrations until the inconsistency is fixed. With multiple databases, you can use allow_migrate() in routers to control which databases are checked.


Adding Migrations to Existing Apps

If an app has models but no migrations (e.g., created before Django 1.7), run:


python manage.py makemigrations your_app
python manage.py migrate --fake-initial

This works only if:

  • Your models have not changed since the tables were created
  • You have not manually modified the database schema

Reversing Migrations

To reverse a migration:


python manage.py migrate books 0002

To reverse all migrations for an app:


python manage.py migrate books zero

Irreversible Migrations

If a migration contains an irreversible operation—such as DROP TABLE—Django raises IrreversibleError when attempting to reverse it.


Conclusion

Advanced migration features—such as transaction control, dependency management, swappable models, and reversible operations—give Django a powerful and reliable schema evolution system. By understanding these concepts, you can manage database changes safely across development, staging, and production environments.

Historical Models in Django Migrations

When Django runs migrations, it does not use your current models. Instead, it loads historical versions of your models from migration files. This ensures that old migrations remain valid even after your models evolve.

If you write Python code inside a RunPython operation or use allow_migrate() in database routers, you must use these historical models via apps.get_model() rather than importing models directly.

Why direct imports are dangerous

Direct imports may work today but will fail when:

  • You set up a new environment
  • You run all migrations from scratch
  • Your current models no longer match the historical schema

If this happens, it’s safe to edit the migration and replace direct imports with historical model access.

Limitations of historical models

Historical models:

  • Do not include custom methods
  • Do not call custom save() methods
  • Do not include custom constructors
  • Only include managers with use_in_migrations = True
  • Preserve historical Meta options

Plan your migration logic accordingly.


Function and Field References in Migrations

If a migration references functions (e.g., upload_to, limit_choices_to) or custom fields, those functions and classes must remain in your codebase as long as the migration exists.

Similarly, if a model inherits from a base class, that base class must remain available until all migrations referencing it are removed or squashed.


Deprecating and Removing Custom Fields

Removing a custom field from your project can break old migrations that reference it. To avoid this, Django provides system check attributes to mark fields as deprecated or removed.

Deprecation example


class IPAddressField(Field):
    system_check_deprecated_details = {
        "msg": "IPAddressField has been deprecated.",
        "hint": "Use GenericIPAddressField instead.",
        "id": "fields.W900",
    }

Removal example


class IPAddressField(Field):
    system_check_removed_details = {
        "msg": "IPAddressField has been removed except in historical migrations.",
        "hint": "Use GenericIPAddressField instead.",
        "id": "fields.E900",
    }

Keep the minimal methods required for migrations—such as __init__(), deconstruct(), and get_internal_type()—until all referencing migrations are gone.


Data Migrations

In addition to schema changes, migrations can modify data. These are called data migrations and are written using RunPython.

Creating an empty migration


python manage.py makemigrations --empty yourappname

Example: combining first and last names


def combine_names(apps, schema_editor):
    Person = apps.get_model("yourappname", "Person")
    for person in Person.objects.all():
        person.name = f"{person.first_name} {person.last_name}"
        person.save()

class Migration(migrations.Migration):
    dependencies = [
        ("yourappname", "0001_initial"),
    ]
    operations = [
        migrations.RunPython(combine_names),
    ]

You may also provide a reverse function for backward migrations. If omitted, reversing will raise an exception.


Accessing Models from Other Apps

If your RunPython function needs models from another app, you must add a dependency on that app’s latest migration.

Example


class Migration(migrations.Migration):
    dependencies = [
        ("app1", "0001_initial"),
        ("app2", "0004_foobar"),  # required for accessing app2 models
    ]
    operations = [
        migrations.RunPython(move_m1),
    ]

Conclusion

Understanding historical models, field deprecation, and data migration patterns is essential for maintaining a stable and future‑proof Django project. By relying on historical models, keeping referenced code available, and writing migrations carefully, you ensure that your project can be installed, upgraded, and maintained reliably across all environments.

Advanced Migration Concepts

Django’s migration system is powerful and flexible. Beyond basic schema and data migrations, Django provides advanced tools for optimizing migration history, serializing complex values, and ensuring long‑term compatibility across Django versions. This article covers the most important advanced features.


Squashing Migrations

As your project grows, the number of migration files increases. Django can handle hundreds of migrations efficiently, but eventually you may want to reduce them for clarity and maintainability. This is where squashing comes in.

What is squashing?

Squashing combines many existing migrations into a single migration (or a small set of migrations) that represents the same final state. Django:

  1. Collects all operations from the migrations being squashed
  2. Places them in sequence
  3. Runs an optimizer to remove redundant operations
  4. Writes a new migration file that replaces the old ones

For example, CreateModel followed by DeleteModel cancels out, and AddField can be merged into CreateModel.

Running squashmigrations


$ python manage.py squashmigrations myapp 0004

The resulting migration:

  • Is marked as replacing the old migrations
  • Can coexist with old migrations
  • Is used automatically for new installations

Recommended workflow:

  1. Squash migrations
  2. Commit both old and new migrations
  3. Deploy and ensure all environments apply the squashed migration
  4. Remove old migrations in a later release

Handling CircularDependencyError

If squashing results in circular dependencies:

  • Move one of the problematic ForeignKey operations into a separate migration
  • Adjust dependencies manually

Serializing Values in Migrations

Migration files are Python code. Django must serialize model definitions and field arguments into valid Python expressions. Django supports serialization of many types, including:

  • Primitive types: int, float, bool, str, bytes, None
  • Containers: list, tuple, set, dict, range
  • Date/time objects
  • UUID, Decimal
  • Enum and Flag instances
  • pathlib paths
  • os.PathLike objects
  • LazyObject wrappers
  • Django fields
  • Top‑level functions and methods
  • Classes at module top level
  • Objects with a custom deconstruct() method

Django cannot serialize:

  • Nested classes
  • Arbitrary class instances
  • Lambdas

Custom Serializers

If Django cannot serialize a value, you can register a custom serializer.

Example: Decimal serializer


class DecimalSerializer(BaseSerializer):
    def serialize(self):
        return repr(self.value), {"from decimal import Decimal"}

MigrationWriter.register_serializer(Decimal, DecimalSerializer)

The serializer must return:

  • A string representation of the value
  • A set of required import statements

Making Custom Classes Serializable with deconstruct()

To serialize custom class instances, implement a deconstruct() method that returns:

  1. path: full Python import path to the class
  2. args: positional arguments for __init__
  3. kwargs: keyword arguments for __init__

Example using @deconstructible


@deconstructible
class MyCustomClass:
    def __init__(self, foo=1):
        self.foo = foo

    def __eq__(self, other):
        return self.foo == other.foo

The decorator automatically captures constructor arguments and ensures Django does not generate unnecessary migrations.


Supporting Multiple Django Versions

If you maintain a third‑party app, you may need migrations that work across multiple Django versions.

Always run makemigrations using the lowest Django version you support.

This ensures that migrations remain compatible with newer versions.


Conclusion

Advanced migration features—such as squashing, serialization, custom serializers, and deconstructible classes—give Django developers powerful tools for managing complex schema evolution. By understanding these mechanisms, you can maintain clean migration histories, support long‑term compatibility, and build robust applications that scale gracefully.

Written & researched by Dr. Shahin Siami