skip to content

In a Django custom Field subclass, what do from_db_value(), to_python(), get_prep_value() and get_db_prep_value() each do, and when is each called?

level: middleimportance: must knowfreq 42%

answer

  1. two directions, four methods
  2. loading versus cleaning
  3. query value versus backend value
  4. values() and aggregates too

basics

~20 s

from_db_value() converts column values into Python objects whenever rows are loaded; to_python() converts form or fixture input during cleaning and deserialization; get_prep_value() turns a Python object into a query value; get_db_prep_value() adds any backend-specific conversion.

solid answer

~30 s

The methods split by direction. Coming **out** of the database, Django calls `from_db_value(value, expression, connection)` for every loaded value, including in `values()` and aggregates, if the field defines it. Coming **in** from users, `to_python(value)` runs during `Model.clean_fields()`/`full_clean()` and deserialization; it must accept an instance of the value class, a string or `None`, and raise `ValidationError` on bad input. Going **into** the database, `get_prep_value(value)` turns a `Money` into `"EUR 12.50"` for saves and for most lookup values; `get_db_prep_value(value, connection, prepared=False)` wraps it and is where connection-specific conversion goes. Pattern lookups such as `contains`, and `iexact`, skip `get_prep_value()`.

code

python · 54 lines
python
from dataclasses import dataclass
from decimal import Decimal, InvalidOperation

from django.core.exceptions import ValidationError
from django.db import models


@dataclass(frozen=True)
class Money:
    amount: Decimal
    currency: str

    def __str__(self):
        return f"{self.currency} {self.amount}"


def parse_money(text):
    try:
        currency, amount = text.split(" ", 1)
        return Money(Decimal(amount), currency)
    except (ValueError, InvalidOperation):
        raise ValidationError(f"Invalid money value: {text!r}") from None


class MoneyField(models.Field):
    description = "An amount and a currency code, stored as 'EUR 12.50'"

    def __init__(self, *args, **kwargs):
        kwargs["max_length"] = 32
        super().__init__(*args, **kwargs)

    def get_internal_type(self):
        return "CharField"  # varchar(32) on every backend

    def from_db_value(self, value, expression, connection):
        if value is None:
            return value
        return parse_money(value)

    def to_python(self, value):
        if isinstance(value, Money) or value is None:
            return value
        return parse_money(value)

    def get_prep_value(self, value):
        value = super().get_prep_value(value)
        if value is None:
            return None
        return str(self.to_python(value))

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        del kwargs["max_length"]  # forced in __init__, so omit for readability
        return name, path, args, kwargs

go deeper

for a junior

Remember the direction of each method: from_db_value in from the database, get_prep_value out to it, to_python for user and fixture input.

for a middle

Explain exactly when each runs, including values() and aggregates for from_db_value, and the lookups that skip get_prep_value.

for a senior

Diagnose half-converted fields, such as raw strings from values() or MySQL type-coercion surprises, and keep the conversions symmetric and None-safe.

for a principal

Decide whether a packed custom column is worth hiding structure from SQL, and require round-trip tests for every custom field the team ships.

## Two directions, four methods A custom Django field converts between a **Python value** (for example `Money(amount=Decimal("12.50"), currency="EUR")`) and a **column value** (the text `"EUR 12.50"` in a `varchar(32)` column). The conversions run in two directions, and Django gives each direction its own methods: | Method | Direction | Called when | Receives | |---|---|---|---| | `from_db_value(value, expression, connection)` | database → Python | every time rows are loaded: model instances, `values()`, `values_list()`, aggregates | the raw column value | | `to_python(value)` | input → Python | `Model.clean_fields()` / `full_clean()`, and deserialization of fixtures | a value object, a string, or `None` | | `get_prep_value(value)` | Python → query value | saving, and preparing most filter values | whatever the attribute or the filter argument holds | | `get_db_prep_value(value, connection, prepared=False)` | query value → backend value | saving (through `get_db_prep_save()`) and lookups | the value, plus the connection in use | ## Loading: `from_db_value()` The base `Field` class does not define `from_db_value()`. If your subclass does, `get_db_converters()` registers it as a converter, and the compiler calls it for each value of that field in each row. It runs for full instances, for `values()` dictionaries and for aggregate results whose output field is your field. It must handle `None` for nullable columns. It should be fast, because it runs once per value — a slow parser here is paid on every list page. ## Cleaning and deserializing: `to_python()` `to_python()` is **not** the loading hook, despite its name. Django calls it from `Field.clean()` — which `Model.clean_fields()` uses, so model forms reach it through `full_clean()` — and from the serializers that load fixtures. Its input can be anything a user or file supplies, so the documented contract is to accept: - an instance of the value class, returned unchanged; - a string, parsed into the value class; - `None`, when the field allows null. When the input is invalid it should raise `django.core.exceptions.ValidationError`, which forms display as a field error. ## Saving and filtering: `get_prep_value()` and `get_db_prep_value()` `get_prep_value()` is the reverse of `from_db_value()`: it turns `Money` into `"EUR 12.50"`. Django uses it in two places: 1. **Saves** — `get_db_prep_save()` calls `get_db_prep_value(prepared=False)`, which calls `get_prep_value()`. 2. **Lookups** — for `exact`, `gt`, `in` and most others, the filter argument goes through `get_prep_value()`, so `Invoice.objects.filter(total=Money(Decimal("12.50"), "EUR"))` compares text to text. Lookups that declare `prepare_rhs = False` — `iexact`, the pattern lookups `contains`/`startswith`/`endswith` and their case-insensitive forms, `isnull`, `regex` — skip `get_prep_value()` and pass the argument through as given. `get_db_prep_value()` receives the **connection**, so it is where backend-specific conversion goes; Django's `BinaryField` wraps bytes in the driver's binary type there. A field that needs one conversion for saving and another for query parameters can override `get_db_prep_save()` as well. ## The pitfalls - **Returning the wrong type from `get_prep_value()`.** On MySQL, comparing a text column with an integer matches unexpectedly; Django's docs say to always return a string for `CHAR`/`VARCHAR`/`TEXT` columns. - **Forgetting `None`.** Both directions see `None` for nullable columns; a parser that calls `.split()` on it fails on the first empty row. - **Parsing in `to_python()` only.** A field with `to_python()` but no `from_db_value()` returns raw strings from queries, because loading never calls `to_python()`. - **Assuming assignment converts.** Setting `invoice.total = "EUR 12.50"` stores the string on the instance; no method runs until the value is saved or the row reloaded. - **Packing hides structure.** Because `MoneyField` stores text, `total__gt` compares strings, not amounts; if the database must compare amounts, store them in a numeric column. ## Testing the round trip Every custom field deserves a small test module that pins each direction separately, because a bug in one direction is invisible from the other: - save a `Money`, reload the row, and compare it with the original — this covers `get_prep_value()` and `from_db_value()` together; - read the same row with `values_list("total", flat=True)` and check that the result is a `Money`, not a string; - call `field.clean("EUR 12.50", None)` and `field.clean("nonsense", None)` to check that `to_python()` parses and rejects correctly; - filter with an exact `Money` and with `total__isnull=True` to cover the lookup path and `None`. These tests run in milliseconds and catch the classic half-converted field long before a list page returns strings.

  • Why do you often call to_python() from inside get_prep_value()?
    Because the value reaching `get_prep_value()` may not be the value class: a filter argument or an attribute set from a string. Normalising through `to_python()` first accepts `Money`, a string or `None` and then serialises one canonical form. Django's own `CharField.get_prep_value()` does the same.
  • Your MoneyField defines to_python() but values() returns raw strings; what is missing?
    `from_db_value()`. Loading, including `values()`, applies only the converters from `get_db_converters()`, and the base `Field` adds `from_db_value` only if the subclass defines it. `to_python()` runs during cleaning and deserialization, never on load.
  • When would you override get_db_prep_value() rather than get_prep_value()?
    When the conversion depends on the database backend or driver, because only `get_db_prep_value()` receives the `connection`. Examples are wrapping bytes in the driver's binary type or choosing a vendor-specific format. Backend-neutral conversion belongs in `get_prep_value()`.

saying these in an interview costs you the question

  • to_python() is what Django calls when it loads rows from the database.
  • from_db_value() is skipped for values() and aggregate results.
  • get_prep_value() runs for every lookup, including contains and iexact.
  • get_db_prep_value() is only for saves; filters never pass through it.
  • to_python() only needs to handle strings, never the value class itself.