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
-
Verify the new migration works:
bash python manage.py migrate --run-syncdb -
Commit both sets (originals + squashed)
-
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
RunPythonin the squashed migration as-is - If the
populate_slugsfunction references models viaapps.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
RunPythonfunctions 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
RunPythonandRunSQLwithout reverse operations - You cannot squash before the first migration if
initial=Truewas 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
💬 Comments (0)
No comments yet
Be the first to share your opinion about this article!