In Django, when you write a custom MoneyField, what does the Field subclass do and what does the model attribute hold?
answer
- two classes, not one
- the converter versus the value
- the field lives in _meta
- instance attribute holds a plain object
basics
~20 sA custom field usually means two classes: a value class such as Money that code works with, and a Field subclass that converts that value to and from its column. The model attribute holds the Money object; the field lives in Model._meta.
solid answer
~40 sA Django `Field` subclass is not what sits on the instance. When the model class is built, each field is recorded in `Model._meta`, and `invoice.total` holds an ordinary Python object. So a custom field is usually two classes: the value class (`Money`, with `amount` and `currency`) that views and services use, and `MoneyField(models.Field)`, which knows how to turn a `Money` into something a column can store and back again: the column type via `get_internal_type()` or `db_type()`, loading via `from_db_value()`, saving and lookups via `get_prep_value()`, forms and deserialization via `to_python()`. Before writing one, check whether a built-in field or a pair of built-ins, such as a `DecimalField` plus a `CharField`, already does the job.
code
python · 15 linesfrom django.db import models
from billing.fields import Money, MoneyField
class Invoice(models.Model):
number = models.CharField(max_length=20, unique=True)
total = MoneyField()
invoice = Invoice.objects.get(number="INV-001")
invoice.total # Money(amount=Decimal('12.50'), currency='EUR')
Invoice._meta.get_field("total") # <billing.fields.MoneyField: total>
invoice.total = Money(invoice.total.amount * 2, "EUR") # plain assignment
invoice.save() # get_prep_value() -> 'EUR 25.00'go deeper
Recall the split: the value class is what your code uses, the Field subclass converts it and describes the column, and the field itself lives in _meta.
Walk through which method runs on load, on save and on form input, and explain why plain assignment triggers none of them.
Argue for or against a custom field versus two native columns, based on whether the database must filter and sort by each part.
Treat a custom field as a long-lived contract that migrations pin, and set rules for when a team may introduce one instead of plain columns.
## Two classes, two jobs Every Django model field is a subclass of `django.db.models.Field`. A frequent misunderstanding is that the field object is what you read and write on a model instance. It is not. When Django builds a model class, it takes each field declared in the class body and records it in the model's **`_meta`** options object. What an instance carries in `invoice.total` is a **plain Python value**: a `str`, a `Decimal`, a `datetime` — or, for a custom field, whatever class you designed. That is why a custom field almost always means **two classes**: - **The value class** — the object application code manipulates. For a price that is `Money(amount=Decimal("12.50"), currency="EUR")`. It knows nothing about databases; it is ordinary Python, often a frozen dataclass with a useful `__str__`. - **The `Field` subclass** — `MoneyField`. It never holds a price. It is the **converter** between `Money` and what a database column can store, and it describes the column itself. ## What the `Field` subclass is responsible for | Concern | Method you override | When Django calls it | |---|---|---| | Which column type to create | `get_internal_type()` or `db_type(connection)` | building `CREATE TABLE` / `ALTER TABLE` | | Turning a loaded column value into `Money` | `from_db_value(value, expression, connection)` | every time rows are loaded, including `values()` and aggregates | | Turning `Money` into a query or save value | `get_prep_value(value)` | saving, and most filter values | | Accepting input from forms or fixtures | `to_python(value)` | `Model.clean_fields()` / `full_clean()` and deserialization | | Recreating the field in migrations | `deconstruct()` | `makemigrations` and migration loading | | Choosing the form field | `formfield(**kwargs)` | building a `ModelForm` or the admin | Only the first row is needed for a field that stores a plain string; the rest become necessary as soon as the Python value is richer than what the column holds. ## Where the value lives at each moment 1. `Invoice.objects.get(pk=1)` — the database returns `"EUR 12.50"`; `from_db_value()` turns it into `Money`, which is stored in `invoice.total`. 2. `invoice.total = Money(Decimal("15.00"), "EUR")` — plain attribute assignment; **no field method runs**. 3. `invoice.save()` — Django reads the attribute and passes it through `get_prep_value()` to get `"EUR 15.00"` for the `UPDATE`. Point 2 surprises people. Before Django 1.10 a metaclass called `SubfieldBase` ran `to_python()` on assignment; it was removed and replaced by `from_db_value()`, which runs only on loading. If code assigns a string — `invoice.total = "EUR 15.00"` — the attribute stays a string until the row is reloaded, and `invoice.total.amount` raises `AttributeError`. Teams that need conversion on assignment add a descriptor to the field; most simply agree to assign `Money` objects. ## Before you write one A custom field is code you own through every future migration: the class must stay importable for as long as any migration references it. So check the cheaper options first: - a **built-in** field already covers it (`DecimalField`, `JSONField`, `UUIDField`, a `CharField` with `choices`); - **two built-in fields** on the model, such as `total_amount = DecimalField(...)` and `total_currency = CharField(max_length=3)`, plus a property that builds `Money` — each column stays filterable and sortable by the database; - a **subclass of a built-in** that changes one behaviour, which inherits its column type, validation and form field. Write a full `Field` subclass when the value really is one unit in one column and you want every load, save and filter to speak `Money` without helper code at each call site. ## Worked picture For `MoneyField`, the column is `varchar(32)` holding text like `"EUR 12.50"`. A service reads `invoice.total.amount`, compares currencies with `invoice.total.currency`, and never touches the text format; only `MoneyField` knows it. That separation is the whole point: the storage format can change inside the field class while the application keeps using `Money`. Forms follow the same split. The default `formfield()` of a `models.Field` subclass is a plain text form field, so a `ModelForm` or the admin shows `str(invoice.total)` — which is why the docs advise giving the value class a `__str__` that produces the storage text. When the form is saved, `full_clean()` calls the field's `to_python()` on the typed text and puts the resulting `Money` back on the instance. A richer editor, such as separate amount and currency inputs, means overriding `formfield()` to return a custom form field.
- If code assigns invoice.total = 'EUR 15.00' as a string, what does invoice.total hold before and after save()?A string both times. Assignment runs no field method, and `save()` only converts a copy for the SQL through `get_prep_value()`; the attribute keeps the string. It becomes `Money` after the row is loaded again, for example with `refresh_from_db()`, because only loading calls `from_db_value()`.
- When is two built-in columns better than one custom MoneyField?When the database must filter, sort or sum by amount, or group by currency, efficiently. A `DecimalField` and a three-character `CharField` keep both parts as native, indexable columns; a property or a small helper builds `Money` in Python. A single packed column is simpler to pass around but hides both parts from SQL.
saying these in an interview costs you the question
- The model instance attribute holds the MoneyField object itself.
- Assigning a string to the attribute runs to_python() and converts it.
- A custom field needs no value class; the Field subclass stores the data.
- from_db_value() also runs when you assign a value in Python.
- Custom fields can be deleted freely once the model stops using them.