In Django, what happens when you unapply a migration whose RunPython has no reverse_code, and when is RunPython.noop the right reverse?
answer
- reversible is a property
- the error names the operation
- a callable that does nothing
- does rollback lose data?
basics
~20 sDjango raises IrreversibleError before undoing any of that migration's operations, so the rollback stops there. RunPython.noop is right when leaving the forward change in place is harmless on the way back; otherwise write a real reverse function.
solid answer
~40 sA `RunPython` without `reverse_code` has `reversible == False`, and when `migrate crm 0007` reaches that migration Django raises `IrreversibleError` ("Operation ... is not reversible") before running any of its operations backwards, so it stays applied. The same holds for `RunSQL` without `reverse_sql`. Passing `migrations.RunPython.noop` as `reverse_code` makes the step reversible by doing nothing, which is right when the forward change needs no undo: backfilling new `first_name` and `last_name` columns while `full_name` still holds the data, since unapplying `0007` drops those columns anyway. It is wrong when the forward code destroyed or moved information the older schema needs, for example once `full_name` has been removed; then the reverse must rebuild it. `RunSQL.noop` is the SQL equivalent.
code
python · 19 linesfrom django.db import migrations
def split_full_name(apps, schema_editor):
Customer = apps.get_model("crm", "Customer")
... # fill first_name and last_name from full_name
class Migration(migrations.Migration):
dependencies = [("crm", "0007_customer_first_name_customer_last_name")]
operations = [
# Safe while full_name still exists: unapplying 0007 drops the new columns.
migrations.RunPython(split_full_name, migrations.RunPython.noop),
migrations.RunSQL(
"CREATE INDEX crm_customer_last_name_lower ON crm_customer (LOWER(last_name));",
reverse_sql="DROP INDEX crm_customer_last_name_lower;",
),
]go deeper
Know that RunPython and RunSQL need a reverse to be undoable and that RunPython.noop and RunSQL.noop exist.
Explain IrreversibleError, that the check runs before the migration's backward pass, and pick noop versus a real reverse for a given data change.
Plan rollbacks: know that later migrations may already be reverted when the error hits, test round trips, and choose deliberate irreversibility for lossy steps.
Set a review policy on reverses that balances rollback safety against the cost and false confidence of reverse code nobody ever runs.
## Reversibility is per operation Every migration operation answers one question through its `reversible` property: can Django undo it? Schema operations like `AddField` know their own inverse. For the two scripted operations Django cannot guess, so reversibility depends on what you pass: | Operation | Reversible when | Irreversible when | |---|---|---| | `RunPython(code, reverse_code=...)` | `reverse_code` is a callable, including `RunPython.noop` | `reverse_code` is omitted (`None`, the default) | | `RunSQL(sql, reverse_sql=...)` | `reverse_sql` is given, including `RunSQL.noop` | `reverse_sql` is omitted (`None`, the default) | `RunPython.noop` is a static method that accepts `(apps, schema_editor)` and returns `None`. `RunSQL.noop` is the empty string, which `RunSQL` treats as "run nothing". ## What rollback does with an irreversible step Rolling back is done with `migrate` and a target, for example `python manage.py migrate crm 0007`, which unapplies every later `crm` migration in reverse order. For each migration being unapplied, Django first walks all of its operations and checks `reversible`. If any is not, it raises `django.db.migrations.exceptions.IrreversibleError` (a `RuntimeError` subclass) with a message naming the operation and the migration. Three consequences follow: 1. **Nothing in that migration is undone.** The check happens before the backward pass starts, so none of its operations run. 2. **The migration stays recorded as applied** in `django_migrations`. 3. **Later migrations may already be reverted.** If `0010` and `0009` were unapplied successfully before Django reached an irreversible `0008`, those stay reverted; you are left part-way down the chain and must decide whether to migrate forward again. This surprises teams during an incident rollback, which is why many reviewers ask for a reverse on every data migration. ## When noop is the right reverse `noop` says "going backwards, leave the data as it is". That is correct when the forward change is harmless to keep: - **Backfilling new columns** that the preceding schema migration added. Unapplying `0008` with `noop` leaves `first_name` and `last_name` filled, and unapplying `0007` then drops them. - **Populating a lookup table** whose rows the older code simply ignores. - **Normalising values** (trimming whitespace, lower-casing emails) where the old code accepts either form. It is the wrong reverse when: - The forward step **deleted, merged or moved** data that the older schema depends on. - The source column has since been **removed**. If `0009` dropped `full_name`, reversing `0009` re-adds the column without its old values, and a `noop` reverse for `0008` leaves it without them. The reverse must rebuild `full_name` from the parts. - The step is genuinely impossible to invert (hashing, truncation). Then leaving it irreversible on purpose is honest: the error is better than a fake reverse that silently corrupts data. ## Writing a useful reverse - Mirror the forward logic with the same historical models: `apps.get_model()`, `.using(schema_editor.connection.alias)`. - Make it tolerate the state the forward step left, including rows created after the forward ran. - Test the round trip on a copy: `migrate crm 0009`, then `migrate crm 0007`, then forward again. ## A related flag: elidable Both operations also accept `elidable=True`, which declares that the operation only mattered at the moment it ran and may be dropped when the migration history is later squashed. It is independent of reversibility: an elidable operation can have a reverse or not. It suits one-off backfills whose effect is already captured in every existing database.
- What does elidable=True on RunPython declare?That the operation's effect only mattered when it ran, so it may be dropped when migrations are squashed. It does not affect whether the operation is reversible or whether it runs on a normal `migrate`; it is a hint for squashing a long history.
- Can you see ahead of time that a rollback will hit an irreversible operation?`migrate crm 0007 --plan` lists the operations it would unapply, and an irreversible `RunPython` or `RunSQL` is flagged in that plan. Checking it before an emergency rollback avoids discovering the problem half-way down the chain.
saying these in an interview costs you the question
- Without reverse_code, rollback just skips the RunPython step
- RunPython.noop deletes the rows the forwards function created
- noop is always a safe reverse for any data migration
- RunSQL without reverse_sql rolls back by doing nothing
- Django can derive the reverse of a RunPython function automatically