---
django: main at 4fab678a0739d54401ccee7eb587553657c9f76e (2026-09-26), on its way to 6.2
---

# Writing a migration file: `MigrationWriter`

A `MigrationWriter` is given a migration and produces the text of a module that will define the same migration when it is imported, `MigrationWriter.as_string`, and the place on disk where that module belongs, `MigrationWriter.path`. Each operation in the text is written by an `OperationWriter`, and each value inside an operation by the serializer.

makemigrations and squashmigrations build migrations as objects in memory, and the writer turns one into Python that will make it again. It never reads a migration file: where one is written a second time, by `makemigrations --update`, which folds new operations into an application's last migration ([The makemigrations command](makemigrations.md)), or by optimizemigration, the text comes whole from the instance, and a comment that stood in the old file is gone. Nor does it write the file: it returns a string and a path, and the command that made it writes the one to the other (`django/db/migrations/writer.py:MigrationWriter`).

## A migration file, part by part

The text of a migration file is a fixed frame with slots, and `MigrationWriter.as_string` fills each, some with text made elsewhere.

```text figure=who-wrote-what
harbour/yard/migrations/0001_initial.py

MIGRATION_HEADER_TEMPLATE, which as_string fills with the version and the time
    # Generated by Django 6.2.dev20260926051415 on (the date and the time)

the imports: asked for by the serialized values, then merged and sorted by as_string
    import django.db.models.deletion
    from django.db import migrations, models


MIGRATION_TEMPLATE: the class line, and the brackets of the two lists
    class Migration(migrations.Migration):

as_string: written only where initial is true
        initial = True

MIGRATION_TEMPLATE
        dependencies = [
as_string: each dependency serialized, the lines sorted
            ('fleet', '0001_initial'),
MIGRATION_TEMPLATE
        ]

        operations = [
OperationWriter: the class, then each argument by name on a line of its own
            migrations.CreateModel(
                name='Dock',
OperationWriter: an argument named in serialization_expand_args, an item to a line
                fields=[
the serializer: the text of each value, and the import the text needs
                    ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
                    ('name', models.CharField(max_length=40)),
                    ('port', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='fleet.port')),
OperationWriter
                ],
            ),
            (the CreateModel of Repair)
MIGRATION_TEMPLATE
        ]
```

*Yard's first migration as makemigrations wrote it, the date and the time left out of its first line, with each part beside what wrote it.*

How a value becomes an expression is in [Serializing values: `serializer_factory` and `deconstruct`](serializer.md): `MigrationWriter.serialize` returns the serializer's answer for a value, its text and the import lines the text needs, and `MigrationWriter.register_serializer` and `MigrationWriter.unregister_serializer` only call `Serializer.register` and `Serializer.unregister` (`django/db/migrations/serializer.py:Serializer`).

## `MigrationWriter.as_string`: the text

`as_string` fills the slots of `MIGRATION_TEMPLATE`, whose own text is the class line and the brackets of the two lists (`django/db/migrations/writer.py:MigrationWriter.as_string`). For each operation of the migration it makes an `OperationWriter`, whose `OperationWriter.serialize` returns the operation's text and its import lines.

A dependency is a key and is serialized as the tuple it is, with one exception. The autodetector gives the dependency that a foreign key to the user model makes the label `"__setting__"` and, in the place of a migration's name, the name of the setting, `AUTH_USER_MODEL` ([From operations to migrations: dependencies, order and names](dependencies.md)). For that label `as_string` writes a call of `swappable_dependency` on the setting and adds `from django.conf import settings` to the imports. The call is made at each import, so the file never fixes the application the setting names; it returns a `SwappableTuple`, the label of the setting's model with `"__first__"`, carrying the setting's value (`django/db/migrations/migration.py:swappable_dependency`). The dependencies' lines are sorted as text.

In the recordings here, from a running Django, a line after a bar is a line of a text or a file and a line with `->` a question and its answer; only the lines a passage is about are written. For a migration whose one dependency is `("__setting__", "AUTH_USER_MODEL")`, a writer's text has these lines:

```text recording=migrations
| from django.conf import settings
...
|         migrations.swappable_dependency(settings.AUTH_USER_MODEL),
```

The label is all the writer goes by. A migration loaded from a file holds the `SwappableTuple` the call returned, and a writer given that migration writes a plain tuple, which fixes the application the setting named when the file was loaded. makemigrations had written the call in the file of yard's ninth migration. As the loader made that migration, with `django.contrib.auth` installed, its dependencies have these types, and a writer's text for it these lines:

```text recording=migrations
[type(dependency).__name__ for dependency in migration.dependencies]  ->  ['tuple', 'SwappableTuple']
...
  |     dependencies = [
  |         ('auth', '__first__'),
  |         ('yard', '0008_alter_repair_foreman'),
  |     ]
```

Of a migration's other attributes, `Migration.replaces` and `Migration.run_before` are written where they are not empty, `Migration.initial` where it is true and `Migration.atomic` where it is false; `dependencies` and `operations` always. The header, `MIGRATION_HEADER_TEMPLATE`, is one comment line with the answer of `get_version` and the time to the minute from `django.utils.timezone.now`, which is UTC where `USE_TZ` is true; a writer made with `include_header=False`, as under `--no-header`, writes none.

## The imports, and functions to be copied by hand

The import lines are a set of strings, gathered as the operations and the dependencies were written, and `MigrationWriter.as_string` changes the set before it writes it (`django/db/migrations/writer.py:MigrationWriter.as_string`).

First it takes out every `import` line in which a name after a dot begins with a digit: the test is for a migration module named as makemigrations names a new one, number first. A `RunPython` whose function is defined in such a module asks for such a line, since a function is serialized as its module and its name with an import of the module, and no `import` statement or dotted expression can name a module such as yard's 0004_name_the_foremen. The writer does not copy the function's source. It drops the line, sets `MigrationWriter.needs_manual_porting` for the command to read ([Who makes a writer](#who-makes-a-writer)), and lists the modules in a comment under the imports; the operation's text still names the function by its module, so the file cannot be imported until someone has done what the comment asks. The comment in the file squashmigrations wrote for yard, and its `RunPython`:

```text recording=migrations
| # Functions from the following migrations need manual copying.
| # Move them and any dependencies into this file, then update the
| # RunPython operations to refer to the local versions:
| # harbour.yard.migrations.0004_name_the_foremen
...
|         migrations.RunPython(
|             code=harbour.yard.migrations.0004_name_the_foremen.name_foremen,
|             reverse_code=harbour.yard.migrations.0004_name_the_foremen.forget_foremen,
|         ),
```

Then `from django.db import models`, where a value asked for it, is replaced by `from django.db import migrations, models`; where none did, `from django.db import migrations` is added, which the class line needs. Last, the sort: `import` lines before `from` lines, each kind by the module it names.

## `OperationWriter`: an operation as a call

An `OperationWriter` builds one operation's text with `OperationWriter.feed`, which appends a line at the writer's level of indentation (four spaces to a level; by default 2, inside the class and its list), and `OperationWriter.render`, which joins the lines (`django/db/migrations/writer.py:OperationWriter`). `OperationWriter.serialize` calls the operation's `Operation.deconstruct`, which returns a name, positional arguments and keyword arguments (`django/db/migrations/operations/base.py:Operation.deconstruct`; [Operations: the base class and its two halves](operations.md)), and asks `get_func_args` for the names of the constructor's parameters. Where `django.db.migrations` has an attribute of that name and it is the operation's class, the call opens with `migrations.` and the name, leaning on the import every file has; otherwise with the class's module and the name, and an import of the module is added.

Every argument is written by keyword and begins a line. A positional argument takes the name of the parameter in its position. Keyword arguments follow in the order of the constructor's signature, and only those that are parameters of it: one that `deconstruct` returns under another name is left out, and nothing is raised.

How an argument is laid out depends on `Operation.serialization_expand_args`, the names of the arguments to spread over lines. One named there whose value is a list, a tuple or a dictionary is opened on a line, and each item, or each key with its value, is serialized on a line of its own, one level in; any other argument is serialized whole after its name. The base class's list is empty, and these fill it: `CreateModel` (`"fields"`, `"options"`, `"managers"`), `AlterModelManagers` (`"managers"`) and `SeparateDatabaseAndState` (its two lists). A value whose text has several lines is fed line by line at the writer's level, which is how the operations inside a `SeparateDatabaseAndState` come out indented under it: each is written by an `OperationWriter` of its own that begins at level 0 (`django/db/migrations/serializer.py:OperationSerializer`).

## Where the file goes: `MigrationWriter.basedir`

`MigrationWriter.filename` is the migration's name and `.py`, and `MigrationWriter.path` joins it to `basedir`, a property: the directory of the application's migrations package, which may not exist yet (`django/db/migrations/writer.py:MigrationWriter.basedir`).

It asks `MigrationLoader.migrations_module` for the package's name: the application's entry in `MIGRATION_MODULES`, or the application's own package and `"migrations"` ([Finding migrations: `MigrationLoader`](loader.md)). For an entry of `None`, an application with no migrations, `basedir` raises `ValueError`. A package that can be imported, and whose directory `module_dir` can tell, answers with it. Of the others, a package directly inside the application's own, as the default one is, has its place under `AppConfig.path`, in a directory of its last name: `basedir` returns that path and makes nothing, and makemigrations makes the directory, with its `__init__.py` (`django/core/management/commands/makemigrations.py:Command.write_migration_files`; [The makemigrations command](makemigrations.md)). What is left is a package that `MIGRATION_MODULES` puts elsewhere: `basedir` drops names from the right of the dotted name until what remains can be imported and has a directory, makes under it the directory of each name it dropped, writes an empty `__init__.py` into each, over one that is there, and returns the last, or raises `ValueError` where no part of it does both.

> **Trap.** Reading `path` is then enough to make directories, and `Command.write_migration_files`, at verbosity 1 and above, reads it to print a file's name before it looks at whether the run is a dry one: `makemigrations --dry-run` can leave a package behind, or empty the `__init__.py` of one that cannot be imported.

## Who makes a writer

A `MigrationWriter` is made by makemigrations ([The makemigrations command](makemigrations.md)), by squashmigrations ([Squashed migrations: `replaces` and the squashmigrations command](squashing.md)) and by optimizemigration ([The optimizer: `MigrationOptimizer` and `reduce`](optimizer.md)).

| command | makes a writer | where `MigrationWriter.needs_manual_porting` is set | a `SwappableTuple` in a migration it loaded |
|---|---|---|---|
| makemigrations | for each migration it is to write; for a merge (`django/core/management/commands/makemigrations.py:Command.handle_merge`); under `--update`, one more only to read the path of the file it supersedes | under `--update`, once the new file is written, warns and keeps the old one, which it otherwise deletes (`django/core/management/commands/makemigrations.py:Command.write_migration_files`) | under `--update`, left as loaded (`django/core/management/commands/makemigrations.py:Command.write_to_last_migration_files`) |
| squashmigrations | for the replacing migration (`django/core/management/commands/squashmigrations.py:Command.handle`) | says so, above verbosity 0 | replaced by `("__setting__", "AUTH_USER_MODEL")` where it carries the value of `AUTH_USER_MODEL` |
| optimizemigration | for the migration it writes again and, where that needs manual porting, another for the one it writes instead (`django/core/management/commands/optimizemigration.py:Command.handle`) | raises `CommandError` for a migration that itself replaces others; for any other leaves the file alone and writes a migration that replaces it | left as loaded |

*What each command makes a writer for, what it does about functions to be copied by hand, and what becomes of a dependency that was a call of `swappable_dependency` in the file it loaded.*
