📝 Django

Squashing Migrations in Django

P
Author
Pyland
📅
Published
30.06.2026
⏱️
Reading time
2 min
👁️
Views
92
🌳
Level
Advanced

Squash — compressing multiple migrations into one to simplify the history.

When to Squash

  • You have 50+ accumulated migrations — the history is unwieldy
  • Development of a feature is complete and you want a clean slate
  • Speeding up test runs (fewer migration files to load)

How to Squash

# Squash migrations from 0001 to 0050 into one
python manage.py squashmigrations tasks 0001 0050

# With a custom result name
python manage.py squashmigrations tasks 0001 0050 --squashed-name initial_squashed

This creates the file 0001_squashed_0050_initial_squashed.py.

After Squashing

  1. Verify the new migration works:
    bash python manage.py migrate --run-syncdb

  2. Commit both sets (originals + squashed)

  3. Once all environments have applied the squashed migration, remove the originals:
    bash # Remove replaces from the squashed file # Delete the original migration files

When NOT to Squash

Squash is an irreversible operation with risks. Avoid it in these cases:

Active deployment or rolling update. If servers are currently applying old migrations and the squashed one hasn’t been applied yet — a conflict will arise. Django won’t be able to determine which migrations have already been applied.

Team has branches with old migrations. If a colleague created a new migration on top of 0045 and you squashed 0001–0050, they’ll get a conflict on merge. All branches must be merged into main first.

Production database with a long migration history. If not all environments (staging, prod, local machines) have applied the same set of migrations — squash will create inconsistency. Make sure python manage.py showmigrations shows the same state everywhere.

RunPython in Squashed Migrations

If the migrations being squashed contain RunPython or RunSQL — squash will include them, but problems can arise:

# This operation in the original migration...
migrations.RunPython(populate_slugs, reverse_code=migrations.RunPython.noop)
  • Django will include RunPython in the squashed migration as-is
  • If the populate_slugs function references models via apps.get_model — that’s fine
  • If the function imports models directly (from .models import Task) — it will break: the model may have changed by the time the squash runs on a new database
  • Always review RunPython functions manually after squashing

Safe way to write a RunPython for squash:

def populate_slugs(apps, schema_editor):
    # Use apps.get_model, not a direct import
    Task = apps.get_model('tasks', 'Task')
    for task in Task.objects.filter(slug=''):
        task.slug = slugify(task.title)
        task.save()

Testing After Squashing

After creating the squashed migration, always verify:

# 1. Run all tests — nothing should break
python manage.py test

# 2. Check a clean install (simulates a fresh environment)
python manage.py migrate --run-syncdb

# 3. Confirm the squashed migration applies from scratch
# Create a test database, apply only the squashed migration
python manage.py migrate tasks 0001_squashed_0050_initial_squashed

If tests pass and the clean migration works — the squash is correct.

Safely Deleting Old Migrations

You can delete the original migrations only when all environments have applied the squashed migration:

# 1. Confirm all environments applied the squashed migration
python manage.py showmigrations tasks  # should show [X] next to squashed

# 2. Open the squashed file and remove the replaces attribute
# replaces = [('tasks', '0001_initial'), ('tasks', '0002_...'), ...]
# After removal Django no longer treats it as a replacement for the old migrations

# 3. Delete the original files
find . -path "*/tasks/migrations/0[0-4]*.py" -delete

# 4. Commit the changes
git add .
git commit -m "chore: remove squashed original migrations for tasks app"

If you delete the originals too early, environments that haven’t applied them yet won’t be able to reconstruct the migration history.

Limitations

  • Squash doesn’t work with RunPython and RunSQL without reverse operations
  • You cannot squash before the first migration if initial=True was used
  • Be careful with custom operations

Alternative: Reset Migrations (Development Only)

# Only if the database is being created from scratch!
find . -path "*/migrations/0*.py" -delete
python manage.py makemigrations
python manage.py migrate --fake-initial

Speeding Up Tests Without Squashing

# pytest.ini
[pytest]
# Use --reuse-db to avoid recreating the database on every run
pytest --reuse-db

Your reaction to the article

💬 Comments (0)

🔐 Sign in to leave a comment
🚪 Login
💭

No comments yet

Be the first to share your opinion about this article!

🔗 Similar

Similar articles

Continue learning with these materials

📝

pytest-django: Testing Django

Охватываемые темы: Installation, @pytest.mark.djangodb, Fixtures, Testing views.

📅 30.06.2026 👁️ 138
📝

Django: Template Tags

Template tags are logic inside HTML. Unlike {{ variable }} which only outputs a value,...

📅 30.06.2026 👁️ 86
📝

Django: Static Files

Static files are CSS, JavaScript, images, and fonts. Django handles them in a specific way:...

📅 30.06.2026 👁️ 77

Did you like the article?

Subscribe to our updates and receive new articles first. Grow with PyLand!