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?
answer
- borrow before you define
- internal type maps per backend
- connection.vendor for raw types
- None means Django skips the column
basics
~10 sSubclass 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 sStart 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 linesfrom 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, kwargsgo deeper
Know that a custom field needs a column type and can usually borrow one by subclassing a built-in field.
Compare subclassing, get_internal_type() and db_type(), including how the backend data type map turns an internal type into SQL.
Handle portability with connection.vendor, rel_db_type() for key fields, and the migration trap of changing a custom field's base class.
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.