Field choices, TextChoices, and IntegerChoices
coreintermediatechoices= restricts a field to a fixed set of values and renders as a dropdown in forms/admin. models.TextChoices and models.IntegerChoices are Python enum subclasses that define the set as named class attributes — MEMBER = value, "Label" — so the valid options live in one typed, importable place instead of a loose list of tuples.
Think of it as
A plain choices= list of tuples is a lookup table Django accepts as data. TextChoices/IntegerChoices turn that same table into a real Python Enum — every member becomes an attribute you can import and reference (Order.Status.PAID), not just a string you have to spell correctly. The database still only ever sees the raw value ("PAID", not the enum member) — the enum is a typed, autocomplete-friendly wrapper around exactly the same choices= list Django always supported.
What we're doing: Define an order status as a TextChoices enum and reference a specific status from application code without a bare string.
- 2
- Nesting Status inside Order keeps the enum scoped to the model it belongs to — referenced elsewhere as Order.Status, not a loose module-level name.
- 8
- Order.Status.PAID is a real Python object with autocomplete and a typo-proof reference — compare to the error-prone alternative of writing the bare string "PAID" everywhere it is checked or assigned.
Why this works: A bare choices= list of tuples still works, but every place that checks or sets a status has to spell the raw string correctly with no help from the editor or a type checker; TextChoices turns the same values into named, importable attributes, catching a typo'd status string at review time (or via a linter) instead of at runtime.
Comparing against the enum member instead of unwrapping .value in a raw SQL or serialization boundary
Wrong
Better
What you see: The raw SQL query silently returns zero rows, or a database driver raises a type-adaptation error, even though Order.objects.filter(status=Order.Status.PAID) works fine through the ORM.
Why: Django's own ORM knows how to unwrap a TextChoices/IntegerChoices member to its underlying value automatically when building a query, but raw SQL, a database driver, or a non-Django serializer does not — those boundaries need the member's .value explicitly, since the enum member itself is not the same object type the database driver expects.
- .value — "PAID" — what the database stores
- .label — "Paid" — the display text
- .name — "PAID" — the attribute name
Defining a set of choices
Together
Remember: TextChoices/IntegerChoices are Enum subclasses wrapping the same choices= list Django always accepted — reference members as Class.Enum.MEMBER for typed autocomplete, and only .value crosses into raw SQL or non-Django serialization.
See also: display and migrations · primary keys and field options · text and numeric fields

