Understanding makemigrations, migrate, and Django’s Migrations Directory

This article explains how Django uses migrations to keep model definitions and database schemas synchronized. It covers the roles of makemigrations and migrate, the structure of each app’s migrations directory, migration dependencies and operations, inspection commands, data migrations, conflicts, common mistakes, and recommended development practices.

Understanding makemigrations, migrate, and Django’s Migrations Directory

Django models define the structure of an application’s data, but editing a model does not directly change the database.

When a developer adds a field, removes a field, creates a model, or changes a relationship, Django needs a controlled way to update the database schema. Django handles these changes through a system called migrations.

The two commands used most often are:

bash

1
python manage.py makemigrations

and:

bash

1
python manage.py migrate

Although these commands are usually run one after the other, they perform different jobs.

What Is a Migration?

A migration is a Python file that describes a change to the database structure.

For example, suppose an application starts with this model:

python

1
2
3
4
5
from django.db import models


class Book(models.Model):
    title = models.CharField(max_length=200)

Later, a publication date is added:

python

1
2
3
class Book(models.Model):
    title = models.CharField(max_length=200)
    publication_date = models.DateField(null=True, blank=True)

The database table does not automatically gain a publication_date column just because the model was edited.

Django must first create a migration describing the change. That migration must then be applied to the database.

The general workflow is:

  1. Change a model.
  2. Run makemigrations.
  3. Review the generated migration.
  4. Run migrate.
  5. Verify that the application still works correctly.

What Does makemigrations Do?

The makemigrations command examines the current model definitions and compares them with Django’s existing migration history.

It then creates new migration files for any detected changes.

Run it with:

bash

1
python manage.py makemigrations

Django may produce output similar to:

bash

1
2
3
Migrations for 'library':
  library/migrations/0002_book_publication_date.py
    + Add field publication_date to book

This tells the developer that Django created a migration for the library app. The migration adds a field named publication_date to the Book model.

makemigrations Does Not Update the Database

An important distinction is that makemigrations usually does not execute database schema changes.

It only detects and creates instructions for those changes.

After running makemigrations, the database may still have its old structure. The generated migration must be applied with migrate.

A useful way to remember the difference is:

  • makemigrations creates the plan.
  • migrate carries out the plan.

What Changes Can makemigrations Detect?

Django can generate migrations for many common model changes, including:

  • Creating a model
  • Deleting a model
  • Adding a field
  • Removing a field
  • Renaming a field
  • Changing a field type
  • Changing field options
  • Adding or removing relationships
  • Adding database constraints
  • Adding database indexes
  • Changing model metadata that affects the database

For example, changing this:

python

1
title = models.CharField(max_length=100)

to this:

python

1
title = models.CharField(max_length=250)

may produce an AlterField migration operation.

Running makemigrations for a Specific App

When you run:

bash

1
python manage.py makemigrations

Django will look for changes inside all your app's models.

However, Django also gives you the option to only look for changes in specific apps. This comes in useful, if one app's changes are ready for deployment and another app is still under development.

To create migrations for one app, include its app label:

bash

1
python manage.py makemigrations library

It can also make migration output easier to understand because Django focuses on changes associated with the specified app.

When Django Asks for Additional Information

Some model changes require information that Django cannot safely determine on its own.

For example, suppose a model already contains database records:

python

1
2
class Book(models.Model):
    title = models.CharField(max_length=200)

A required field is then added:

python

1
2
3
class Book(models.Model):
    title = models.CharField(max_length=200)
    page_count = models.PositiveIntegerField()

Existing rows do not have a value for page_count. Because the field does not allow NULL and has no default, Django may ask how existing records should be populated.

The command might offer choices such as, after running makemigrations:

bash

1
2
1) Provide a one-off default now
2) Quit and manually define a default value in models.py

The developer must decide how old records should be handled.

A common solution is to temporarily provide a default:

python

1
page_count = models.PositiveIntegerField(default=0)

Another option is to make the field nullable:

python

1
page_count = models.PositiveIntegerField(null=True, blank=True)

The correct choice depends on the application’s data rules.

What Does migrate Do?

The migrate command applies pending migrations to the database.

Run it with:

python

1
python manage.py migrate

Django checks which migrations have already been applied and executes any that are still pending.

Example output might look like:

bash

1
2
3
4
5
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, library, sessions

Running migrations:
  Applying library.0002_book_publication_date... OK

In this example, Django applies migration 0002_book_publication_date from the library app.

Depending on the migration, Django may create tables, add columns, remove columns, create indexes, or perform other database operations.

Django Tracks Applied Migrations

Django records applied migrations in a database table named:

django_migrations

This table allows Django to determine which migration files have already been executed.

Because of this tracking system, running:

bash

1
python manage.py migrate

multiple times does not repeatedly apply the same migration.

When no migrations are pending, Django may report:

bash

1
No migrations to apply.

Running migrate for a Specific App

Just like makemigrations, migrate runs for all apps by default.

To apply migrations for one app, specify the app label:

bash

1
python manage.py migrate library

Django will apply the required migrations for that app, including any dependencies that must be completed first.

A particular migration can also be targeted:

bash

1
python manage.py migrate library 0002

This tells Django to move the library app’s migration state to migration 0002.

If later migrations have already been applied, Django may reverse them where possible.

This is useful if you want to revert changes not by just altering models.py but first reverting to the old functional version, make your changes, and then apply changes to an old baseline.

For example:

bash

1
2
3
python manage.py migrate library 0001

could reverse migrations after 0001.

Migration reversal should be handled carefully, especially in production databases, because reversing schema changes may delete columns, tables, or data.

The Difference Between makemigrations and migrate

As mentioned before: The commands are related, but they are not interchangeable.

Command Main purpose:

  • makemigrations: Creates migration files based on model changes
  • migrate: Applies migration files to the database

Suppose a developer adds a price field to a model.

Running only:

bash

1
python manage.py makemigrations

creates the migration file, but the database does not yet contain the new column.

Running only:

bash

1
python manage.py migrate

does not create a migration for an unrecorded model change. It only applies migrations that already exist.

The usual sequence is therefore:

  1. python manage.py makemigrations
  2. python manage.py migrate

The migrations Directory

Each Django app that uses migrations normally contains a directory named migrations.

A typical app structure looks like this:

    library/
    ├── migrations/
    │   ├── __init__.py
    │   ├── 0001_initial.py
    │   └── 0002_book_publication_date.py
    ├── admin.py
    ├── apps.py
    ├── models.py
    ├── tests.py
    └── views.py

The migrations directory stores the migration history for that app.

Django apps maintain separate migration directories because each app is responsible for its own models and schema changes.

Migration File Names

Migration files generally use names such as:

bash

1
2
3
0001_initial.py
0002_book_publication_date.py
0003_alter_book_title.py

The numeric prefix indicates the order of the migrations within the app.

The descriptive portion provides a summary of the change.

The first migration is commonly named:

bash

1
0001_initial.py

It usually creates the app’s initial database tables.

Later migration files describe additional changes.

The filename helps developers understand the approximate sequence, but migration dependencies inside the files determine the actual execution order.

The Structure of a Migration File

A generated migration file might look like this:

bash

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ("library", "0001_initial"),
    ]

    operations = [
        migrations.AddField(
            model_name="book",
            name="publication_date",
            field=models.DateField(blank=True, null=True),
        ),
    ]

The two most important sections are:

  • dependencies
  • operations
Migration Dependencies

The dependencies list identifies migrations that must be applied before the current migration.

For example:

dependencies = [ ("library", "0001_initial"), ]

This means the current migration depends on the library app’s 0001_initial migration.

A migration may also depend on a migration from another app if model references exist.

For example:

dependencies = [ ("authors", "0002_author_profile"), ]

Dependencies allow Django to build a migration graph rather than relying only on filenames.

This is especially important when models in different apps have relationships with one another.

Migration Operations

The operations list describes what the migration should do.

Common operations include:

bash

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
migrations.CreateModel(...)
migrations.DeleteModel(...)
migrations.AddField(...)
migrations.RemoveField(...)
migrations.AlterField(...)
migrations.RenameField(...)
migrations.AddConstraint(...)
migrations.RemoveConstraint(...)
migrations.AddIndex(...)
migrations.RemoveIndex(...)

For example:

python

1
2
3
4
5
6
7
operations = [
    migrations.AddField(
        model_name="book",
        name="publication_date",
        field=models.DateField(null=True, blank=True),
    ),
]

This operation tells Django to add a publication_date field to the Book model’s database table.

Why Migration Files Should Be Committed to Version Control

Migration files are part of a Django project’s source code.

They should normally be committed to Git or another version-control system.

This ensures that:

  1. Other developers can reproduce the same database structure.
  2. Test and production environments receive the same changes.
  3. Deployment systems can apply migrations consistently.
  4. The project retains a documented schema history.

A developer should not usually add the migrations directory to .gitignore.

Consider a team where one developer adds a field and runs makemigrations. If the generated migration file is not committed, other developers may receive the changed model but not the instructions needed to update their databases.

The expected workflow is:

  1. python manage.py makemigrations
  2. git add library/migrations/
  3. git commit -m "Add publication date to Book"

Other developers can then pull the changes and run:

bash

1
python manage.py migrate

Should Migration Files Be Edited Manually?

Generated migration files can be edited manually, but beginners should do so cautiously.

Django-generated migrations are usually correct for common schema changes.

Manual editing may be appropriate when:

  • Writing a custom data migration
  • Adjusting dependencies
  • Using advanced database operations
  • Optimizing a complex migration
  • Resolving a migration conflict

Incorrect manual changes can leave the migration history and database schema inconsistent.

Before editing a migration manually, it is important to understand the effect of each operation and test the migration on a non-production database.

Data Migrations

Not all migrations change table structure.

A data migration changes the data stored inside existing tables.

For example, suppose a new slug field is added to an Article model. Existing articles may need slug values generated from their titles.

An empty migration can be created with:

python

1
python manage.py makemigrations blog --empty --name populate_article_slugs

This creates a migration file that can be customized.

A simplified data migration may look like this:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from django.db import migrations
from django.utils.text import slugify


def populate_slugs(apps, schema_editor):
    Article = apps.get_model("blog", "Article")

    for article in Article.objects.all():
        article.slug = slugify(article.title)
        article.save(update_fields=["slug"])


class Migration(migrations.Migration):

    dependencies = [
        ("blog", "0002_article_slug"),
    ]

    operations = [
        migrations.RunPython(populate_slugs),
    ]

Inside a migration, models should generally be retrieved through:

apps.get_model()

This gives the migration access to the historical version of the model that corresponds to that point in the migration sequence.

Importing the current model directly can cause older migrations to break after the model changes again.

Inspecting Migration Status

Django provides several commands for inspecting migrations.

showmigrations

Usage:

bash

1
python manage.py showmigrations

This displays the migrations for each app.

Example:

bash

1
2
3
4
library
 [X] 0001_initial
 [X] 0002_book_publication_date
 [ ] 0003_book_price

An [X] indicates that a migration has been applied.

An empty checkbox indicates that the migration is pending.

To inspect one app:

bash

1
python manage.py showmigrations library

sqlmigrate

The sqlmigrate command displays the SQL Django expects to run for a migration.

For example:

bash

1
python manage.py sqlmigrate library 0002

This can help developers understand how a migration maps to database-level operations.

The exact SQL varies depending on the configured database backend.

makemigrations --check

In automated testing or continuous integration, the following command can check whether model changes exist without corresponding migration files:

bash

1
python manage.py makemigrations --check

A nonzero exit status indicates that new migrations would need to be created.

This helps prevent developers from committing model changes while forgetting the associated migration files.

Migration Conflicts

Migration conflicts can occur when multiple developers create migrations from the same previous migration.

For example, one branch may create:

bash

1
0003_book_price.py

while another branch creates:

bash

1
0003_book_isbn.py

After the branches are merged, the app has two migrations with the same parent.

Django may report conflicting migrations.

In some cases, the conflict can be resolved with:

bash

1
python manage.py makemigrations --merge

Django creates a merge migration that depends on both branches.

The generated result should be reviewed carefully, particularly when the migrations alter the same fields or perform incompatible operations.

Why Deleting Migration Files Can Be Dangerous

Beginners sometimes delete migration files when an error occurs. This can create additional problems.

The database may already record those migrations as applied. Deleting the files does not automatically remove their effects from the database or from the django_migrations table.

This can lead to several inconsistencies:

  • Django may think the schema is at one state while the database is at another.
  • Other developers may still have the deleted migrations.
  • Production may contain migration history that no longer exists in the codebase.
  • Future migrations may refer to missing dependencies.

During very early local development, resetting migrations and recreating the database may sometimes be acceptable. It is much more dangerous once the project has shared, important, or production data.

Migration files should not be deleted casually.

Avoid Changing Applied Migrations

Once a migration has been shared or applied in another environment, it should generally be treated as historical.

Suppose migration 0002 has already been applied in development, testing, and production. Editing that file later does not automatically cause Django to apply the new version because Django already records 0002 as completed.

Instead, create a new migration containing the additional change.

A good rule is:

Do not rewrite migration history after it has been shared. Add a new migration.

Schema Migrations and Data Safety

Some migrations can cause data loss.

Examples include:

  • Removing a field
  • Deleting a model
  • Changing an incompatible field type
  • Reversing a migration that created important data
  • Adding a required field without a valid strategy for existing rows

Before applying migrations in production:

  • Back up important data.
  • Review the migration file.
  • Consider the generated SQL.
  • Test the migration on realistic data.
  • Check whether the operation locks a large table.
  • Confirm that application code and database changes are deployed in a compatible order.

Migration safety becomes increasingly important as an application and its database grow.

A Complete Example Workflow

Suppose a library app contains this model:

python

1
2
3
4
5
6
7
8
from django.db import models


class Book(models.Model):
    title = models.CharField(max_length=200)

    def __str__(self):
        return self.title

The developer adds an ISBN:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
class Book(models.Model):
    title = models.CharField(max_length=200)
    isbn = models.CharField(
        max_length=13,
        unique=True,
        null=True,
        blank=True,
    )

    def __str__(self):
        return self.title

The migration is created:

bash

1
python manage.py makemigrations library

Django creates a file similar to:

library/migrations/0002_book_isbn.py

The developer reviews the file:

python

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
from django.db import migrations, models


class Migration(migrations.Migration):

    dependencies = [
        ("library", "0001_initial"),
    ]

    operations = [
        migrations.AddField(
            model_name="book",
            name="isbn",
            field=models.CharField(
                blank=True,
                max_length=13,
                null=True,
                unique=True,
            ),
        ),
    ]

The migration is applied:

bash

1
python manage.py migrate

The migration status is checked:

bash

1
python manage.py showmigrations library

Finally, the model change and migration file are committed together:

bash

1
2
git add library/models.py library/migrations/0002_book_isbn.py
git commit -m "Add ISBN field to Book"

Common Mistakes

Editing a Model Without Creating a Migration

Changing models.py alone does not update the database.

Run:

bash

1
2
python manage.py makemigrations
python manage.py migrate

Running makemigrations but Not migrate

This creates the migration instructions but leaves the database unchanged.

Apply the migration with:

bash

1
python manage.py migrate

Running migrate Without Creating a Needed Migration

The migrate command only applies existing migration files. It does not normally generate new ones from model changes.

Create them first with:

bash

1
python manage.py makemigrations

Forgetting to Commit Migration Files

Other environments need the same migration files to reproduce the database structure.

Commit migrations along with the model changes that produced them.

Deleting Migrations to Fix Every Problem

Deleting migrations can make the migration history inconsistent with the database.

Investigate the underlying issue before removing migration files.

Modifying an Old Shared Migration

Django will not automatically rerun a migration that is already marked as applied.

Create a new migration instead.

Adding a Required Field Without Considering Existing Rows

Existing records need a value for the new field.

Use an appropriate default, allow NULL temporarily, or create a staged data migration.

Django migrations provide a controlled history of database schema changes.

The makemigrations command creates migration files by comparing current models with the recorded migration state. The migrate command applies those files to the database. Each app’s migrations directory stores that app’s ordered migration history, including dependencies and database operations.

The most important distinction is:

  • Models describe the desired data structure.
  • Migration files describe how that structure changes over time.
  • The database reflects those changes only after the migrations are applied.

By reviewing migration files, committing them to version control, and applying them carefully, developers can keep Django models and database schemas synchronized across local, testing, and production environments.

Join the Newsletter

Practical insights on Django, backend systems, deployment, architecture, and real-world development — delivered without noise.

Get updates when new guides, learning paths, cheat sheets, and field notes are published.

No spam. Unsubscribe anytime.



There is no third-party involved so don't worry - we won't share your details with anyone.