How do Django's TextChoices and IntegerChoices work on a model field, and does choices= stop invalid values from reaching the database?
answer
- an enum with labels attached
- value stored, label displayed
- get_FOO_display()
- checked by validation only
- a constraint for the database
basics
~20 sTextChoices and IntegerChoices are enums whose members carry a stored value and a human label; pass the class as choices=. Choices are enforced by model validation and forms only, so save(), update() or raw SQL can still store any value.
solid answer
~40 s`TextChoices` (a `str` enum) and `IntegerChoices` (an `int` enum) let you declare members like `VEGAN = "vegan", "Vegan"`: the value is stored in the column, the label is shown to people, and a member without a label gets one from its name. Since Django 5.0 you pass the class itself, `choices=Diet`; earlier code wrote `Diet.choices`. The field then gets a select widget, `item.get_diet_display()` returns the label, and members compare equal to raw values, so `item.diet == MenuItem.Diet.VEGAN` works after a load from the database. But `choices` is **validation only**: `full_clean()` and `ModelForm`s reject unknown values, while `save()`, `update()`, `bulk_create()` and raw SQL store whatever they are given — the column has no constraint. For database enforcement add a `CheckConstraint` such as `Q(diet__in=Diet.values)` in `Meta`.
code
python · 7 linesitem = MenuItem(diet="keto")
item.save() # stored: save() does not run full_clean()
try:
MenuItem(diet="keto").full_clean()
except ValidationError as exc:
print(exc.message_dict["diet"]) # ["Value 'keto' is not a valid choice."]go deeper
Declare a TextChoices or IntegerChoices class, pass it as choices=, and use get_FOO_display() to show the label.
Explain value versus label, the 5.0 change to pass the class directly, and that choices are checked only by full_clean() and forms.
Decide when a CheckConstraint must back the choices, and plan value renames as data migrations that update rows, constraints and clients together.
Choose between enums in code, constraints and lookup tables for categories that business users may want to change without a deployment.
## Declaring choices as enums Django's enumeration types live in `django.db.models`: `TextChoices` subclasses Python's `StrEnum`, `IntegerChoices` subclasses `IntEnum`, and both add a **label** to every member. ```python from django.db import models from django.utils.translation import gettext_lazy as _ class MenuItem(models.Model): class Diet(models.TextChoices): NONE = "none", _("No restriction") VEGETARIAN = "veg", _("Vegetarian") VEGAN = "vegan", _("Vegan") GLUTEN_FREE = "gf", _("Gluten-free") class Spice(models.IntegerChoices): MILD = 1 MEDIUM = 2 HOT = 3 diet = models.CharField(max_length=5, choices=Diet, default=Diet.NONE) spice = models.PositiveSmallIntegerField(choices=Spice, default=Spice.MILD) ``` What each part does: - **Value** — the first element (`"vegan"`, `1`) is what the column stores. - **Label** — the last element, usually a lazy translation; if omitted, as in `Spice`, Django derives it from the member name (`MILD` becomes `"Mild"`). - **Class helpers** — `Diet.choices` (value/label pairs), `Diet.values`, `Diet.labels`, `Diet.names`, and `Diet.VEGAN.label` on a member. - **Uniqueness** — duplicate values in one choices class are an error when the class is defined. Since Django 5.0, `choices=` accepts the enum class directly, a mapping, or a callable; before that you had to pass `Diet.choices`. The older list-of-pairs form still works. ## What choices changes on the model 1. **Forms** — a `ModelForm` or the admin renders a select box of the labels. 2. **Display** — Django adds `get_diet_display()`, returning the label for the stored value (or the raw value if it is not a known choice). 3. **Validation** — `Model.full_clean()` rejects a value outside the choices with "Value … is not a valid choice." 4. **System checks** — a `CharField` whose `max_length` is shorter than the longest choice value fails check `fields.E009`. 5. **Migrations** — editing the choices produces an `AlterField` migration, but `choices` is a non-database attribute, so it runs no SQL. Because the members are real `str`/`int` values, comparisons work in both directions: a row loaded from the database holds the plain string `"vegan"`, and `item.diet == MenuItem.Diet.VEGAN` is `True`. Filters can use members too: `MenuItem.objects.filter(diet=MenuItem.Diet.VEGAN)`. ## What choices does not do `choices` never reaches the database schema. Nothing stops these from storing `"keto"`: - `MenuItem(diet="keto").save()` — `save()` does not call `full_clean()`; - `MenuItem.objects.update(diet="keto")` and `bulk_create()`; - a data migration, a raw SQL script or another service writing to the table. | protection | where it applies | |---|---| | `choices` | `ModelForm`s, the admin, explicit `full_clean()` | | `CheckConstraint` in `Meta.constraints` | every write, enforced by the database | If the value set must hold for every row, declare a constraint as well: ```python class Meta: constraints = [ models.CheckConstraint( condition=models.Q(diet__in=["none", "veg", "vegan", "gf"]), name="menuitem_diet_valid", ) ] ``` The trade-off: adding a choice then needs a schema migration to widen the constraint, whereas choices alone change without touching the table. ## Using choices in templates and forms - In templates, `{{ item.get_diet_display }}` shows the label. Choices classes set `do_not_call_in_templates`, so `{{ MenuItem.Diet.VEGAN }}` can be referenced without the template engine trying to call the class. - A select box for a field with `blank=True` or no default starts with a blank option. On an enumeration class, set `__empty__ = _("(Not set)")` to label it; a member with the value `None` is not allowed, because every member must match the enum's data type. - Enumeration classes do not support named groups; use a mapping or list of pairs when the select needs optgroups. ## Stored value versus label Store short, stable values and treat labels as presentation. Renaming a label is free; renaming a **value** means migrating existing rows, updating any constraint, and changing every client that sends the old value. Integer choices are compact but opaque in the database, while text values stay readable in SQL and exports.
- What does get_diet_display() return for a value that is not in the choices?The raw stored value itself. Django looks the value up in the field's flattened choices and falls back to the value when there is no match, so a stray `"keto"` displays as `keto` rather than raising.
- Does changing a choice label require a migration?`makemigrations` records an `AlterField`, because choices are part of the field's definition, but it runs no SQL: `choices` is a non-database attribute. Changing a stored **value** is different — existing rows and any constraint must be migrated.
saying these in an interview costs you the question
- choices= creates a database constraint on the column
- save() rejects values that are not in choices
- a loaded row's value no longer equals the enum member
- renaming a choice's stored value needs no data migration
- a TextChoices member must always declare an explicit label