~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:
- Creating a new table
- Copying data
- Dropping the old table
- 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
Metaoptions
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:
- Collects all operations from the migrations being squashed
- Places them in sequence
- Runs an optimizer to remove redundant operations
- 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:
- Squash migrations
- Commit both old and new migrations
- Deploy and ensure all environments apply the squashed migration
- Remove old migrations in a later release
Handling CircularDependencyError
If squashing results in circular dependencies:
- Move one of the problematic
ForeignKeyoperations 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:
- path: full Python import path to the class
- args: positional arguments for
__init__ - 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