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?
answer
- two directions, four methods
- loading versus cleaning
- query value versus backend value
- values() and aggregates too
basics
~20 sfrom_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 sThe 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 linesfrom 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, kwargsgo deeper
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.
Explain exactly when each runs, including values() and aggregates for from_db_value, and the lookups that skip get_prep_value.
Diagnose half-converted fields, such as raw strings from values() or MySQL type-coercion surprises, and keep the conversions symmetric and None-safe.
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.