The Ultimate Guide: Django Migration Error How to Fix It Safely
If you are reading this, chances are you are staring at a red terminal screen, your deployment is blocked, and you are frantically searching for “django migration error how to fix” so you can get your database back on track. Take a deep breath. You are in good company.
Django’s migration system is an incredibly powerful tool for managing database schema changes over time. However, because it acts as a bridge between your Python code and your database engine (PostgreSQL, MySQL, SQLite, etc.), things can occasionally get out of sync. When your database state doesn’t perfectly match what your Django models expect, a migration error is thrown.
In this comprehensive troubleshooting guide, we are going to dive deep into the root causes of Django migration errors. We will walk through step-by-step solutions, starting from the most common scenarios and moving into advanced edge cases. I will share practical, copy-paste-ready code examples and personal experiences to help you resolve these issues safely, whether you are in local development or staring down a production outage.
Understanding Django Migrations and Root Causes
Before we start fixing things, we need to understand why migrations break.
Django tracks the state of your database schema using two primary mechanisms:
1. Migration Files: Python files in your app’s migrations/ directory. They contain operations like CreateModel, AddField, and AlterField.
2. The django_migrations Table: A table in your actual database that records which migration files have been applied.
A migration error almost always occurs because of a discrepancy between these two elements, or because the database itself physically rejects a schema change.
The Most Common Culprits
- Inconsistent Migration History: The
django_migrationsdatabase table says a migration was applied, but the migration file is missing from your codebase (often caused by a bad Git merge). Or, the file exists in the codebase, but the database failed to record it. - Rollback Incompatibilities: You tried to reverse a migration, but the migration file lacks an
inversefunction (common in custom data migrations usingRunPython). - Database Constraint Violations: Django tries to add a
NOT NULLcolumn to a table that already has rows, and the database engine physically rejects it because it doesn’t know what default value to use. - Circular Dependencies: App A depends on App B, but App B also depends on App A. Django gets stuck in an infinite loop trying to resolve the order of execution.
Let’s look at how to fix these issues, step-by-step.
Step-by-Step Solutions: The Standard Fixes
Solution 1: Resolving Inconsistent Migration History
This is the most frequent error developers encounter. You pull the latest code from your repository, run python manage.py migrate, and are greeted with something like this:
django.db.migrations.exceptions.InconsistentMigrationHistory: Migration 'app1.0002_user_age' is applied before its dependency 'accounts.0001_initial' on database 'default'.
The Root Cause:
Django applies migrations in a strict chronological order based on dependencies. If the database thinks 0002 is applied, but 0001 from a different app (which 0002 relies on) is not, Django halts to prevent data corruption.
The Fix:
You need to get your database and code back in sync. First, check what your database actually thinks has happened. Run:
python manage.py showmigrations
Look for the [X] and [ ] marks. If you see an inconsistency, you have two options.
Option A: Fake the Missing Dependency (Safest for Production)
If you know for a fact that the underlying tables actually exist in the database (perhaps they were created manually or via a different process), you can tell Django to record the migration as applied without actually running the SQL.
python manage.py migrate accounts 0001 --fake
Note: Be extremely careful with --fake. If the table doesn’t actually exist in the database, future migrations will fail catastrophically.
Option B: Roll Back and Re-apply (Best for Local Dev)
If you are in a local environment and data preservation isn’t critical, the cleanest fix is to roll back the app that jumped ahead, and then apply everything together.
# Roll back app1 to before the problematic state
python manage.py migrate app1 zero
# Now apply all migrations smoothly
python manage.py migrate
Solution 2: Fixing Merge Conflicts in Migrations
Have you ever branched off main, created a new model, and your colleague also created a new model on main? When you merge your Git branches, Django will likely throw this error:
CommandError: Conflicting migrations detected; multiple leaf nodes in the migration graph.
The Root Cause:
Your 0003_feature_a.py and your colleague’s 0003_feature_b.py both branch off from 0002. Django doesn’t know which one to run first.
The Fix:
Django has a built-in tool specifically designed for this scenario. Open your terminal and run:
python manage.py makemigrations --merge
Django will show you the conflicting branches and ask if you want to create a merge migration. Type y.
This generates a new file (e.g., 0004_merge_20260115_1430.py). It doesn’t contain schema changes; it simply tells Django, “Treat 0003_feature_a and 0003_feature_b as parallel and resolved.” Once created, simply run:
python manage.py migrate
Solution 3: The Phantom Column (Table Already Exists)
Sometimes your local development gets corrupted. You delete some migration files, run makemigrations, run migrate, and get this frustrating error:
django.db.utils.OperationalError: table "myapp_userprofile" already exists
The Root Cause:
Django generated a brand new 0001_initial.py file because you deleted the old ones. But your local SQLite/Postgres database already contains the myapp_userprofile table from previous testing. Django tries to run CREATE TABLE, and the database rejects it.
The Fix:
You need to tell Django that the initial state has already been met. This is the perfect use case for --fake-initial.
python manage.py migrate --fake-initial
When you pass --fake-initial, Django evaluates the CreateModel operations. If it detects that the tables already exist in the database, it will ask you if it should fake the application of those specific initial tables. This safely updates the django_migrations table without attempting to run the CREATE TABLE SQL.
Solution 4: Handling NOT NULL Constraint Failures
You added a required field (null=False, blank=False) to a model that represents a table already populated with thousands of rows.
django.db.utils.ProgrammingError: column "new_field" contains null values
(Or in SQLite):
django.db.utils.OperationalError: Cannot add a NOT NULL column with default value NULL
The Root Cause:
Your existing rows don’t have data for new_field. The database refuses to lock the table to add a NOT NULL column without a default value to fall back on.
The Fix (The Production-Safe Way):
Never try to alter the model directly to add a default. Instead, break this into a three-step process using data migrations.
1. Make the field nullable temporarily:
# models.py
new_field = models.CharField(max_length=100, null=True, blank=True)
Run python manage.py makemigrations and python manage.py migrate.
2. Create an empty data migration to populate the existing rows:
python manage.py makemigrations --empty myapp
3. Write the logic to update the rows:
Open the newly created migration file and use RunPython.
from django.db import migrations, models
def set_default_value(apps, schema_editor):
# We get the model from the historical version in this migration
MyModel = apps.get_model('myapp', 'MyModel')
MyModel.objects.filter(new_field__isnull=True).update(new_field='Default Value')
class Migration(migrations.Migration):
dependencies = [
('myapp', '0004_auto_20260115_1430'),
]
operations = [
migrations.RunPython(set_default_value, migrations.RunPython.noop),
]
Run python manage.py migrate.
4. Enforce the constraint:
Finally, update your model to null=False and remove the default logic.
# models.py
new_field = models.CharField(max_length=100, default='Default Value')
Run makemigrations and migrate one last time. The database will safely apply the NOT NULL constraint because every row now has valid data.
Advanced Scenarios: The Edge Cases
Edge Case 1: The IrreversibleError
You tried to roll back a migration using python manage.py migrate myapp 0001, and you were hit with:
django.db.migrations.exceptions.IrreversibleError: Operation <RunPython <function>> in myapp/0004_data_migration is not reversible
The Root Cause:
Schema changes (like adding a column) are mathematically reversible (drop the column). Data changes (like lowercasing all emails) are often conceptually irreversible (how do you know what the original casing was?). If you use RunPython and don’t provide a reverse function, Django refuses to reverse the migration.
The Fix:
If you are absolutely certain you don’t care about the data integrity and just want to roll back the schema, you can force Django to forget the migration was applied.
python manage.py migrate myapp 0004 --fake
Then, you can safely run python manage.py migrate myapp 0001.
To prevent this in the future, always provide a reverse function for your RunPython operations, even if it’s just a no-op (do nothing) or a strict reversal logic.
def reverse_lowercase_emails(apps, schema_editor):
# Write logic to reverse the change, or simply pass
pass
operations = [
migrations.RunPython(lowercase_emails, reverse_lowercase_emails),
]
Edge Case 2: Custom User Model Nightmares
Switching the AUTH_USER_MODEL midway through a project is the most notorious cause of catastrophic migration errors in Django.
ValueError: The field admin.LogEntry.user was declared with a lazy reference to 'accounts.customuser', but app 'accounts' isn't in INSTALLED_APPS.
(Or similar dependency errors regarding auth.User).
The Root Cause:
The admin, auth, and sessions apps create initial migrations that hardcode a foreign key to auth.User. If you swap the user model later, Django’s migration graph breaks because the historical 0001_initial files for the built-in apps still reference the old user model.
The Fix (The Fresh Start):
If you haven’t gone to production yet, the absolute safest route is to wipe the slate clean.
- Delete all migration files in every app (keep the
__init__.pyfiles).
bash
find . -path "*/migrations/*.py" -not -name "__init__.py" -delete - Drop the database entirely. (e.g.,
dropdb mydb, or delete thedb.sqlite3file). - Update
settings.pyto point to your new `AUTH_USER