Django migrations
last modified November 13, 2026 Python 3.15
In this article we work with Django migrations. We create the initial
migration of a model, change the model, write a data migration with
RunPython, roll migrations back, check for missing migrations, and
squash a series of migrations. The examples use Django 6.1, Python 3.15, and the
default SQLite database.
Django migrations
A migration is a Python file that describes a change of the
database schema: creating a model, adding a field, renaming a column, or
running custom code. Django's models (see Django
models) describe the desired state; migrations are the steps that
bring the database from one state to the next one. They are stored in the
migrations package of an app, numbered, kept under version control,
and applied in the same way in every environment. Django records the applied
migrations in the django_migrations table.
Four commands are used most of the time.
makemigrations- compares the models with the current migrations and writes new migration filesmigrate- applies migrations to the database, or unapplies themshowmigrations- lists migrations and marks the applied onessqlmigrate- prints the SQL of a migration without running it
The example project
The project is created with uv; the steps are explained in
Django introduction. The commands below create a project
directory, install Django, generate the project with the configuration package
config, and start the app inventory.
$ mkdir migrations $ cd migrations $ uv init --bare $ uv add django $ uv run django-admin startproject config . $ uv run python manage.py startapp inventory
The trailing dot in startproject config . creates manage.py and the
config package in the current directory instead of one level deeper. A new app is
ignored by Django until it is listed in INSTALLED_APPS, so we add
the app to the list in config/settings.py; the other
entries are generated by startproject.
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'inventory',
]
Every listing in this article is a file of the application, shown with a solid blue border and the label Project file.
We start with a model of a product that has a name and a price.
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=8, decimal_places=2)
def __str__(self):
return self.name
We will change this model step by step and let the migrations follow.
The initial migration
The makemigrations command inspects the models and writes a
migration for every change that is not yet covered by existing migrations. For a new
app, the result is the initial migration. The first line of the output names the
app, the second the new file, and the following lines list the operations in it.
$ uv run python manage.py makemigrations inventory
Migrations for 'inventory':
inventory/migrations/0001_initial.py
+ Create model Product
Anatomy of a migration
The generated file is ordinary Python code. It has a header comment with the
Django version and the time, a Migration class, the
dependencies list of migrations that must be applied first, and the
list of operations. It should be reviewed and committed together with
the model change.
# Generated by Django 6.1.3 on 2026-11-13 20:02
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Product',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100)),
('price', models.DecimalField(decimal_places=2, max_digits=8)),
],
),
]
The initial = True attribute marks the first migration of the app.
The CreateModel operation lists the fields, including the automatically added
id primary key, which is a BigAutoField by default in
the current Django versions. The dependencies are empty, because the app has no
relation to other apps; if our models used User, the migration
would depend on the migrations of the auth app.
The SQL of a migration
Before we touch the database, we can look at the SQL that the migration
would run with the sqlmigrate command. It takes the app label and
the migration name; the number is enough. The SQL depends on the database
backend, here SQLite. This is a good way to review a migration, especially
before running it on a production database.
$ uv run python manage.py sqlmigrate inventory 0001
BEGIN;
--
-- Create model Product
--
CREATE TABLE "inventory_product" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "name" varchar(100) NOT NULL, "price" decimal NOT NULL);
COMMIT;
Applying migrations
The migrate command applies the unapplied migrations. The
project also has migrations of the built-in apps (authentication, sessions,
and the admin site), which are applied at the same time. After that,
showmigrations lists every migration with a mark: [X]
means applied. We limit the output to our app.
$ uv run python manage.py migrate Operations to perform: Apply all migrations: admin, auth, contenttypes, inventory, sessions Running migrations: Applying contenttypes.0001_initial... OK Applying auth.0001_initial... OK Applying admin.0001_initial... OK Applying admin.0002_logentry_remove_auto_add... OK Applying admin.0003_logentry_add_action_flag_choices... OK Applying contenttypes.0002_remove_content_type_name... OK Applying auth.0002_alter_permission_name_max_length... OK Applying auth.0003_alter_user_email_max_length... OK Applying auth.0004_alter_user_username_opts... OK Applying auth.0005_alter_user_last_login_null... OK Applying auth.0006_require_contenttypes_0002... OK Applying auth.0007_alter_validators_add_error_messages... OK Applying auth.0008_alter_user_username_max_length... OK Applying auth.0009_alter_user_last_name_max_length... OK Applying auth.0010_alter_group_name_max_length... OK Applying auth.0011_update_proxy_permissions... OK Applying auth.0012_alter_user_first_name_max_length... OK Applying inventory.0001_initial... OK Applying sessions.0001_initial... OK $ uv run python manage.py showmigrations inventory inventory [X] 0001_initial
Sample data
Before we change the model, we insert two products with the old model, so that the table is not empty and the effect of the following migrations is visible.
$ uv run python manage.py shell --no-imports -c "from inventory.models import Product; Product.objects.create(name='Desk lamp', price='19.90'); Product.objects.create(name='Office chair', price='89.00')"
Adding a field
Next we add a sku (stock keeping unit) field to the model. A new
column of an existing table needs a value for the rows that already exist, so a
field that cannot be NULL needs a default; without it,
makemigrations asks for a one-off default in an interactive prompt. We give the default in the
model.
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=8, decimal_places=2)
sku = models.CharField(max_length=20, default='')
def __str__(self):
return self.name
The model has the new field. We create and examine the migration; the name of the migration
file, 0002_product_sku, is generated from the operation. The SQLite
backend rebuilds the table for this change, so the SQL creates a new table, copies
the rows into it, and renames it.
$ uv run python manage.py makemigrations inventory
Migrations for 'inventory':
inventory/migrations/0002_product_sku.py
+ Add field sku to product
$ uv run python manage.py sqlmigrate inventory 0002
BEGIN;
--
-- Add field sku to product
--
CREATE TABLE "new__inventory_product" ("id" integer NOT NULL PRIMARY KEY AUTOINCREMENT, "sku" varchar(20) NOT NULL, "name" varchar(100) NOT NULL, "price" decimal NOT NULL);
INSERT INTO "new__inventory_product" ("id", "name", "price", "sku") SELECT "id", "name", "price", '' FROM "inventory_product";
DROP TABLE "inventory_product";
ALTER TABLE "new__inventory_product" RENAME TO "inventory_product";
COMMIT;
$ uv run python manage.py migrate inventory
Operations to perform:
Apply all migrations: inventory
Running migrations:
Applying inventory.0002_product_sku... OK
Data migrations
Both products have an empty sku now. A data migration
changes the rows instead of the schema. We create an empty migration with the
--empty option and fill it with a RunPython operation. The
operation takes a forward function and, optionally, a reverse function, so the
migration can be rolled back.
$ uv run python manage.py makemigrations inventory --empty --name fill_sku Migrations for 'inventory': inventory/migrations/0003_fill_sku.py
The RunPython operation
The empty migration has no operations. We edit the file and add the
functions and the RunPython operation.
from django.db import migrations
def fill_sku(apps, schema_editor):
Product = apps.get_model('inventory', 'Product')
for product in Product.objects.filter(sku=''):
product.sku = product.name.upper().replace(' ', '-')[:20]
product.save(update_fields=['sku'])
def clear_sku(apps, schema_editor):
Product = apps.get_model('inventory', 'Product')
Product.objects.update(sku='')
class Migration(migrations.Migration):
dependencies = [
('inventory', '0002_product_sku'),
]
operations = [
migrations.RunPython(fill_sku, clear_sku),
]
The functions receive the apps registry and the schema editor. We must
get the model with apps.get_model() instead of importing it: the
function works with the historical version of the model, as it was at this point
of the migration history, not with the current class, which may have more or fewer
fields. The forward function builds a SKU from the product name; the reverse
function empties the field. The forward function touches only the rows with
an empty SKU, so running it again is harmless. Data migrations should not call model
methods that are defined in the current code, because the historical model has no
custom methods.
$ uv run python manage.py migrate inventory
Operations to perform:
Apply all migrations: inventory
Running migrations:
Applying inventory.0003_fill_sku... OK
$ uv run python manage.py shell --no-imports -c "from inventory.models import Product; print(list(Product.objects.values_list('name', 'sku')))"
[('Desk lamp', 'DESK-LAMP'), ('Office chair', 'OFFICE-CHAIR')]
Unapplying migrations
The migrate command with an app label and a migration number
moves the database to the state after that migration. When the number is lower than
the current state, Django unapplies the later migrations in the reverse
order and calls the reverse functions. Here we go back to 0002:
the data migration is reversed, and the SKU values are cleared. The word
zero unapplies all migrations of the app.
$ uv run python manage.py migrate inventory 0002
Operations to perform:
Target specific migration: 0002_product_sku, from inventory
Running migrations:
Rendering model states... DONE
Unapplying inventory.0003_fill_sku... OK
$ uv run python manage.py shell --no-imports -c "from inventory.models import Product; print(list(Product.objects.values_list('name', 'sku')))"
[('Desk lamp', ''), ('Office chair', '')]
$ uv run python manage.py showmigrations inventory
inventory
[X] 0001_initial
[X] 0002_product_sku
[ ] 0003_fill_sku
The migration plan
With the --plan option, migrate prints the migrations
it would apply, without applying them. This is useful in deployment scripts. After
the command we apply the migration again.
$ uv run python manage.py migrate inventory --plan
Planned operations:
inventory.0003_fill_sku
Raw Python operation
$ uv run python manage.py migrate inventory
Operations to perform:
Apply all migrations: inventory
Running migrations:
Applying inventory.0003_fill_sku... OK
Checking for missing migrations
It is easy to change a model and forget to create the migration. The
--check option makes makemigrations exit with a non-zero status
when there are model changes without a migration; combined with
--dry-run nothing is written. This command fits a continuous
integration pipeline. We add an in_stock field to the model and
run the check; the shell variable $? holds the exit status of the
command.
from django.db import models
class Product(models.Model):
name = models.CharField(max_length=100)
price = models.DecimalField(max_digits=8, decimal_places=2)
sku = models.CharField(max_length=20, default='')
in_stock = models.BooleanField(default=True)
def __str__(self):
return self.name
The check reports the pending change and exits with the status 1. We then create and apply the migration, and the check passes with the status 0.
$ uv run python manage.py makemigrations --check --dry-run; echo "exit status: $?"
Migrations for 'inventory':
inventory/migrations/0004_product_in_stock.py
+ Add field in_stock to product
exit status: 1
$ uv run python manage.py makemigrations inventory
Migrations for 'inventory':
inventory/migrations/0004_product_in_stock.py
+ Add field in_stock to product
$ uv run python manage.py migrate inventory
Operations to perform:
Apply all migrations: inventory
Running migrations:
Applying inventory.0004_product_in_stock... OK
$ uv run python manage.py makemigrations --check --dry-run; echo "exit status: $?"
No changes detected
exit status: 0
Squashing migrations
A long-lived project accumulates hundreds of migrations, and new databases have
to run all of them. The squashmigrations command merges a range of
migrations into one by optimizing the operations. The new file has a
replaces list with the original migrations: databases where they are
all applied treat the squashed migration as applied, and new databases run only
the squashed one. The command takes the app label, the first migration of the
range (optional), and the last one. After all databases have applied the squashed
migration, the replaced files can be deleted and the replaces
attribute removed.
We squash only the first two migrations. The two schema operations are
optimized into a single CreateModel that already contains the
sku field. Squashing a range that contains a RunPython
operation needs care: when we tried squashmigrations inventory 0001 0004
with Django 6.1.3, the command wrote a file that refers to the function as
inventory.migrations.0003_fill_sku.fill_sku, which is not valid Python
because a module name cannot start with a digit, and every later command
failed with a SyntaxError. The workaround is to squash only the schema
migrations or to copy the functions into the squashed migration by hand. Right
after the squash, showmigrations marks the squashed migration with
[-], partly applied; the following migrate only records
it as applied, because all the migrations it replaces are applied. The original
files stay on disk, and the squashed migration is used for new installs.
$ uv run python manage.py squashmigrations inventory 0001 0002 --noinput Will squash the following migrations: - 0001_initial - 0002_product_sku Optimizing... Optimized from 2 operations to 1 operations. Created new squashed migration ./inventory/migrations/0001_initial_squashed_0002_product_sku.py You should commit this migration but leave the old ones in place; the new migration will be used for new installs. Once you are sure all instances of the codebase have applied the migrations you squashed, you can delete them. $ ls inventory/migrations | grep -v pycache 0001_initial.py 0001_initial_squashed_0002_product_sku.py 0002_product_sku.py 0003_fill_sku.py 0004_product_in_stock.py __init__.py $ uv run python manage.py showmigrations inventory inventory [-] 0001_initial_squashed_0002_product_sku (2 squashed migrations) Run 'manage.py migrate' to finish recording. [X] 0003_fill_sku [X] 0004_product_in_stock $ uv run python manage.py migrate inventory Operations to perform: Apply all migrations: inventory Running migrations: No migrations to apply. $ uv run python manage.py showmigrations inventory inventory [X] 0001_initial_squashed_0002_product_sku (2 squashed migrations) [X] 0003_fill_sku [X] 0004_product_in_stock
Testing migrations
A simple but valuable test calls makemigrations --check --dry-run
through call_command(); it raises SystemExit when a
migration is missing, which fails the test. The test runner creates the test
database by running all migrations, so the second test also proves that the
migrations produce the schema that the model needs.
from django.core.management import call_command
from django.test import TestCase
from .models import Product
class MigrationTests(TestCase):
def test_no_missing_migrations(self):
call_command('makemigrations', '--check', '--dry-run')
def test_product_defaults(self):
product = Product.objects.create(name='Desk lamp', price='19.90')
self.assertEqual(product.sku, '')
self.assertTrue(product.in_stock)
The second test checks the defaults of the new fields on a product created through the migrated schema.
$ uv run python manage.py test Found 2 test(s). System check identified no issues (0 silenced). No changes detected Creating test database for alias 'default'... .. ---------------------------------------------------------------------- Ran 2 tests in 0.004s OK Destroying test database for alias 'default'...
Source
Django documentation: Migrations
Django documentation: Migration operations
Django documentation: squashmigrations
In this article we have created and applied migrations, reviewed their SQL,
added a field, written a reversible data migration with RunPython,
rolled migrations back, checked for missing migrations, and squashed a series of
migrations. The models that migrations describe are covered in
Django models; the next article,
Django QuerySets, shows how to query the data.
Author
List all Django tutorials.