ZetCode

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.

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.

config/settings.py
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.

inventory/models.py
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.

inventory/migrations/0001_initial.py
# 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.

inventory/models.py
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.

inventory/migrations/0003_fill_sku.py
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.

inventory/models.py
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.

inventory/tests.py
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Django tutorials.