skip to content

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%

answer

  1. borrow before you define
  2. internal type maps per backend
  3. connection.vendor for raw types
  4. None means Django skips the column

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.

solid answer

~40 s

Start from the most specific reuse. If a built-in such as `DecimalField` already has the right column and validation, subclass it and change only behaviour. If you subclass `models.Field` but the storage is a known type, return that field's name from `get_internal_type()`, for example `"CharField"`: each backend's data type map then produces `varchar(32)` from `max_length`, and serialisers see the name. Override `db_type(connection)` when you need a type Django does not map, such as a PostgreSQL custom type, and branch on `connection.vendor` for portability; returning `None` makes Django skip the column so you create it yourself. A key type that foreign keys point at also needs `rel_db_type()`.

code

python · 19 lines
python
from django.db import models


class CurrencyCodeField(models.Field):
    """An ISO 4217 code in a fixed-width column."""

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

    def db_type(self, connection):
        if connection.vendor == "sqlite":
            return "varchar(3)"
        return "char(3)"

    def deconstruct(self):
        name, path, args, kwargs = super().deconstruct()
        del kwargs["max_length"]
        return name, path, args, kwargs

go deeper

for a junior

Know that a custom field needs a column type and can usually borrow one by subclassing a built-in field.

for a middle

Compare subclassing, get_internal_type() and db_type(), including how the backend data type map turns an internal type into SQL.

for a senior

Handle portability with connection.vendor, rel_db_type() for key fields, and the migration trap of changing a custom field's base class.

for a principal

Weigh vendor-specific column types against portability and plan how a custom field's storage can evolve without rewriting migration history.

## Three ways to get a column Every concrete Django field must end up as a column type the database understands. A custom field can get there in three ways, from the least code to the most: | Approach | What you write | What you get for free | Typical use | |---|---|---|---| | **Subclass a built-in field** | `class MoneyAmountField(models.DecimalField)` | column type, validators, form field, lookups | changing behaviour on top of a standard type | | **`get_internal_type()`** | return `"CharField"` from a `models.Field` subclass | the per-backend type from Django's data type map | a new Python type stored in a standard column | | **`db_type(connection)`** | return a raw SQL type string | nothing: full control | a database type Django does not map | ## Subclassing a built-in Subclassing is the default the Django docs recommend: find the existing field closest to what you want and extend it. A subclass of `DecimalField` inherits `max_digits`/`decimal_places`, the `DecimalValidator`, the form field and every numeric lookup. The trade-off is that you also inherit its assumptions. A `CharField` subclass, for example, appends a `MaxLengthValidator` that calls `len()` on the value — which breaks if your field stores a non-string value object such as `Money`. In that case, subclass `models.Field` and borrow only the column. ## `get_internal_type()`: borrowing a column Each database backend has a **data type map** keyed by internal type names — `"CharField"` maps to `varchar(%(max_length)s)`, `"DecimalField"` to a numeric type with precision and scale, and so on. The default `db_type()` looks up `self.get_internal_type()` in that map and fills the placeholders from the field's attributes. So a `MoneyField(models.Field)` that sets `max_length = 32` and returns `"CharField"`: - gets `varchar(32)` on every built-in backend; - reports `CharField` to serialisers; - needs no vendor-specific code. If `get_internal_type()` returns a name the backend does not know, the default `db_type()` returns `None` — the string is still used by serialisers. ## `db_type(connection)`: raw control Override `db_type()` when no internal type fits: a PostgreSQL custom type, a fixed `char(3)` currency code, or a vendor-specific type. Practical rules: 1. **Branch on `connection.vendor`** — built-in vendor names are `sqlite`, `postgresql`, `mysql` and `oracle`. 2. **Parameterise** — take sizes from `__init__` arguments rather than hard-coding them, as `char(%s)` with `self.max_length`. 3. **Return `None` to opt out** — Django then skips the column in its schema SQL, and you create it yourself, typically with `RunSQL`. 4. **Pair with `rel_db_type()`** — if the field can be a primary key, `ForeignKey` and `OneToOneField` columns pointing at it call `rel_db_type()` to get a compatible type; an unsigned auto field needs an unsigned foreign key column. `db_type()` is called when Django builds `CREATE TABLE` and `ALTER TABLE` statements, and when a query needs the column's type, for example in a cast. ## A decision you cannot easily undo The base class of a custom field is effectively part of its migration history. Django does not detect a change from `class MoneyField(models.CharField)` to `class MoneyField(models.TextField)`: the import path and arguments are the same, so no migration is written and the column stays as it was. The documented way is a **new field class** — `MoneyTextField` — pointed to by the model, which produces a real `AlterField`, with the old class kept while migrations reference it. Choose the column deliberately on day one. ## Checking the result After choosing, verify what Django will actually emit rather than trusting the class definition. `python manage.py sqlmigrate billing 0001` prints the SQL for a migration, including the column type each field produced, and `Invoice._meta.get_field("total").db_type(connection)` returns the type for the current connection in a shell. Doing this on each backend the project supports catches a vendor branch that returns the wrong string before it reaches production. ## Choosing for a Money field - **Numeric amount, one currency per column** → subclass `DecimalField`; currency lives in the field definition. - **Amount and currency packed per row** → `models.Field` plus `get_internal_type()` returning `"CharField"` and a fixed `max_length`. - **A PostgreSQL composite type** → `db_type()` returning the type name on `postgresql` and a fallback elsewhere, with the type created in a migration.

  • Why can subclassing CharField break a field whose Python value is a Money object?
    `CharField` adds a `MaxLengthValidator`, and `full_clean()` runs validators on the value returned by `to_python()`. The validator calls `len()` on a `Money` object, which fails unless the class defines `__len__`. Subclassing `models.Field` and returning `"CharField"` from `get_internal_type()` borrows the column without that validator.
  • What does returning None from db_type() do?
    Django's schema SQL skips the column entirely, so `migrate` creates no column for that field. You then create it yourself, typically with a `RunSQL` operation, which suits types needing setup Django cannot express.

saying these in an interview costs you the question

  • db_type() must always be overridden for a custom field to work.
  • get_internal_type() can return any string and Django creates that SQL type.
  • Changing a custom field's base class makes makemigrations write an AlterField.
  • connection.vendor for PostgreSQL is 'postgres'.
  • Subclassing CharField is always safe for any custom Python value type.