๐Ÿ“ Django

Squashing Migrations in Django

P
Author
PyLand Team
๐Ÿ“…
Published
30.06.2026
โฑ๏ธ
Reading time
2 min
๐Ÿ‘๏ธ
Views
338
๐ŸŒณ
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

๐Ÿ“

Search in Django

You can start with simple Django ORM filters and move to PostgreSQL features when you...

๐Ÿ“… 30.06.2026 ๐Ÿ‘๏ธ 280
๐Ÿ“

Swagger Documentation with drf-spectacular

drf-spectacular generates an OpenAPI 3.0 schema and Swagger UI for DRF.

๐Ÿ“… 30.06.2026 ๐Ÿ‘๏ธ 565
๐Ÿ“

settings.py: Django Configuration

settings.py is the central configuration file for a Django project.

๐Ÿ“… 30.06.2026 ๐Ÿ‘๏ธ 316

Did you like the article?

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