skip to content

Why does a Django custom model field need a correct deconstruct() method, and when must you override it?

level: middleimportance: should knowfreq 35%

answer

  1. migrations rebuild the field
  2. a four-item tuple
  3. whatever __init__ changed
  4. omit defaults, keep it importable

basics

~20 s

Django's migrations record every field as the arguments needed to rebuild it, and deconstruct() supplies them as (name, path, args, kwargs). Override it whenever your init adds or forces arguments, so migrations can recreate the exact field.

solid answer

~40 s

Migrations never pickle a field; they write Python that constructs it again, and `deconstruct()` returns what that code needs: a four-item tuple of attribute name, import path, positional arguments and keyword arguments. The base `Field.deconstruct()` already covers every standard option, so a subclass that adds nothing needs no override. You override it when `__init__` changes the arguments: add a new keyword such as `currencies` to `kwargs` (omitting it when it holds the default), and drop an argument you force, such as `max_length`, for readability. The rule is that `MoneyField(*args, **kwargs)` built from the returned values must recreate the same state. Getting it wrong shows up as a missing migration, a `TypeError` when migrations load, or a `ValueError` from the migration writer.

code

python · 22 lines
python
from django.db import models


class MoneyField(models.Field):
    def __init__(self, *args, currencies=("EUR",), **kwargs):
        self.currencies = tuple(currencies)
        kwargs["max_length"] = 32
        super().__init__(*args, **kwargs)

    @property
    def non_db_attrs(self):
        return super().non_db_attrs + ("currencies",)

    def get_internal_type(self):
        return "CharField"

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        del kwargs["max_length"]              # forced by __init__
        if self.currencies != ("EUR",):       # emit only non-defaults
            kwargs["currencies"] = list(self.currencies)
        return name, path, args, kwargs

go deeper

for a junior

Remember that migrations rebuild each field from the arguments deconstruct() returns, so options your field adds must appear there.

for a middle

Explain the four-item tuple, when overriding is required, and why defaults are omitted while new options are added explicitly.

for a senior

Recognise the symptoms of a wrong deconstruct(), such as missing migrations, TypeErrors on load and unserialisable values, and guard it with a round-trip test.

for a principal

Treat a custom field's import path and arguments as a contract that old migrations pin, and plan renames and base-class changes as new classes.

## Why migrations need it Django's **migration framework** records the state of every model as Python code: each `CreateModel` or `AddField` operation contains a call such as `billing.fields.MoneyField(currencies=["EUR", "GBP"])`. To write that code, the autodetector asks each field instance how to rebuild itself, through **`deconstruct()`**. The same values are compared between the last migration's state and the current models; a difference produces an `AlterField`. `deconstruct()` returns a **four-item tuple**: 1. the field's **attribute name** on the model; 2. the **import path** of the field class, such as `billing.fields.MoneyField`; 3. **positional arguments**, as a list; 4. **keyword arguments**, as a dict. The base `Field` works out the first two and fills the last two with every standard option that differs from its default — `null`, `blank`, `default`, `max_length`, `db_column` and the rest. This is a different signature from the three-item `deconstruct()` used for custom classes that appear as argument values. ## When you must override it If your subclass accepts only the standard options, the inherited method is complete. Override it when **`__init__` changes the arguments**: - **You add a keyword.** `MoneyField(currencies=("EUR", "GBP"))` stores `self.currencies`; the base method knows nothing about it, so you must put it into `kwargs` yourself. Omit it when it equals the default, so migrations stay short and stable. - **You force a value.** `MoneyField.__init__` sets `kwargs["max_length"] = 32`. The base method will report `max_length=32`; deleting it from `kwargs` keeps migrations readable. Leaving it in is harmless, because `__init__` overwrites it anyway. - **You change a superclass default.** If the subclass makes `blank=True` its default, always put `blank` into `kwargs`, whatever its value: the base method omits values equal to the *base* default, so an explicit `blank=False` would vanish and be rebuilt as `True`. The invariant to test: `MoneyField(*args, **kwargs)` built from the returned values must reproduce the field's state. ## What goes wrong | Mistake | Symptom | |---|---| | New kwarg not added to `kwargs` | changing it never produces a migration; historical models in data migrations see the default | | `kwargs` contains a key `__init__` does not accept | `TypeError` as soon as migrations are loaded | | A value the writer cannot serialise, such as a lambda | `makemigrations` fails with `ValueError` ("Cannot serialize") | | Field class moved, renamed or deleted | old migrations cannot import it; keep the class, or a stub at its path, as long as migrations reference it | | Base class changed from `CharField` to `TextField` | Django detects no change; create a new field class instead | ## How the tuple is used The import path is built from the class's module and qualified name, and Django shortens it only for its own core fields, which is why migrations say `models.CharField` but `billing.fields.MoneyField`. Two practical consequences follow: - the field class must live at **module top level** in an importable module, so the path in the migration resolves; a field class defined inside a function cannot be imported back; - the path is **frozen into every migration** that mentions the field, so moving `MoneyField` to another module means leaving an import at the old path, or editing old migrations, for as long as they exist. The arguments are compared as values. Keeping them to plain literals — strings, numbers, lists, tuples, dicts, `Decimal` — makes comparison and serialisation predictable; a custom object used as an argument needs its own three-item `deconstruct()` so the writer can express it. ## Two related tools - **`non_db_attrs`** (Django 4.1+) — a tuple of attribute names that do not affect the column. Adding `"currencies"` to it tells the schema editor that an `AlterField` changing only `currencies` needs no SQL. - **A round-trip test** — deconstruct and reconstruct in a unit test, then compare attributes. It catches a forgotten kwarg long before a teammate's data migration behaves strangely. ## Worked example `MoneyField` gains a `currencies` option that limits which codes `to_python()` accepts. The column is unchanged, so the option is validation-only: it goes into `kwargs` when it differs from `("EUR",)`, into `non_db_attrs`, and into a test that rebuilds the field from `deconstruct()` and compares `currencies`.

  • What happens if deconstruct() forgets to include a new keyword argument?
    Migrations record the field without it, so historical models rebuild it with the default, and because old and new states deconstruct identically, changing the option never creates a migration. If the option affects the column, the database silently keeps the old definition.
  • Why can't you simply switch a custom field's base class from CharField to TextField?
    Django will not detect the change and write a migration for it, because the field's path and arguments are unchanged. Create a new field class such as `MoneyTextField`, point the model at it so an `AlterField` is generated, and keep the old class while any migration references it.

saying these in an interview costs you the question

  • deconstruct() is only used by the admin to display field options.
  • Every custom field must override deconstruct(), even one adding no options.
  • Leaving a forced max_length in deconstruct()'s kwargs breaks migrations.
  • Changing an option that deconstruct() omits still triggers a new migration.
  • deconstruct() returns a three-item tuple of path, args and kwargs.