skip to content

Custom Column Types

Subclassing Field: db_type, from_db_value, get_prep_value, deconstruct() for migrations and custom lookups via register_lookup. Interviewers probe the value round trip between Python and SQL.

part ofDjangooverview, primer and where to startread it →
on this pageshow

explore

questions

5

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.
open as a page

In Django, when you write a custom MoneyField, what does the Field subclass do and what does the model attribute hold?

level: juniorimportance: should knowfreq 32%

basics

~20 s

A 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.

open as a page

For a Django custom model field, how do you choose its database column type: db_type(), get_internal_type(), or subclassing a built-in field?

level: middleimportance: should knowfreq 25%

basics

~10 s

Subclass a built-in field when its column and validation already fit; return a built-in name from get_internal_type() to borrow its per-backend column type; override db_type(connection) only for a type Django lacks, branching on connection.vendor.

open as a page

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

level: middleimportance: should knowfreq 35%

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.

open as a page

With Django's register_lookup(), how would you let queries filter a custom MoneyField by currency, as in total__currency='EUR', and why use a Transform?

level: seniorimportance: nice to knowfreq 20%

basics

~20 s

Write a Transform subclass with lookup_name = 'currency', an SQL template extracting the code and output_field = CharField(), then register it with MoneyField.register_lookup(). A Transform yields a value, so exact, in and order_by chain after it.

open as a page