Django Better Choices
Better choices library for Django web framework.
Define your choices once, get:
- String values (for DB compatibility) + rich attributes (for app logic).
- Pretty display labels (lazy-translatable).
- Typed access via enum members.
- Subsets (extract/exclude) for forms/admin or feature flags.
- Django-friendly:
ModelField(choices=..., default=...).
Requirements
This library was written for Python 3.9+ and may not work in any earlier versions.
Installation
pip install django-better-choices
# or with uv
uv add django-better-choices
Quick start
from django_better_choices import Choices
class Status(Choices):
# simplest: display only — value is auto-generated as lowercase("name")
DRAFT = "Draft"
# explicit wrapper — value auto-generated later
OPEN = Choices.Value("Open")
# fully explicit
CLOSED = Choices.Value("Closed", value="closed-hard", css="badge--red")
# attach arbitrary params to a choice (become attributes)
PENDING = Choices.Value("Pending review", level=2, css="badge--yellow")
# subsets (reusable groups of members)
PUBLIC = Choices.Subset("OPEN", "CLOSED")
# Use like enum + str
Status.OPEN.value # "open"
Status.OPEN.display # "Open"
Status.PENDING.level # 2
str(Status.OPEN) # "open"
Status("open") is Status.OPEN # True
"open" in Status # True
# For Django model fields:
# choices=Status.choices() -> [("draft", "Draft"), ("open", "Open"), ...]
Why this instead of plain Enum or Django tuples?
- Single source of truth: define once, get both the DB value (string/bool/int) and the human label.
- Attributes per choice: attach metadata (
css,level, ...) right on the member. - Subsets: build narrow groups (
PUBLIC,INTERNAL, ...) or compose at runtime (extract,exclude). - Typed access: members are enum-like and
str-like, so they fit Django fields and your logic.
Usage
Defining values & parameters
1) Implicit values (default)
If you pass a string, it’s treated as display. The stored value is the lower-cased member name.
class Example(Choices):
FOO = "Foo"
BAR = "Bar"
Example.FOO.value # "foo"
Example.FOO.display # "Foo"
2) Choices.Value(display, *, value=_auto, **params)
display: what users see (may be lazy_("Text")).value: hashable value (string/bool/int). By default, auto-generated from the member name.params: any extra attributes you want to access later.
class Example(Choices):
A = Choices.Value("Alpha", slug="alpha", css="badge")
B = Choices.Value("Beta", value=True, risk=5)
C = Choices.Value("Gamma", value=7)
Example.A.slug # "alpha"
Example.B.value # True
Example.C.value # 7
3) Custom value factory (optional)
Override _choices_value_factory_ to control auto values.
class Example(Choices):
_choices_value_factory_ = staticmethod(lambda name, **_: name.upper())
ALPHA = "Alpha"
Example.ALPHA.value # "ALPHA"
Subsets
Create named subsets on the class:
class Status(Choices):
OPEN = "Open"
CLOSED = "Closed"
ARCHIVED = "Archived"
ACTIVE = Choices.Subset("OPEN", "CLOSED")
Or ad-hoc at runtime:
Status.extract("OPEN", "CLOSED") # -> new Choices subclass "Status.Subset"
Status.exclude("ARCHIVED") # -> everything but ARCHIVED
Subsets are full Choices classes themselves:
subset = Status.ACTIVE
list(subset) # [Status.OPEN, Status.CLOSED]
subset.choices() # [("open", "Open"), ("closed", "Closed")]
repr(subset) # "<choices 'Status.ACTIVE'>"
Django integration
Model fields
from django.db import models
class Ticket(models.Model):
class Status(Choices):
OPEN = "Open"
CLOSED = Choices.Value("Closed", css="badge--red")
status = models.CharField(
max_length=20,
choices=Status.choices(),
default=Status.OPEN, # can pass the member (stores its .value)
)
Forms/admin
from django import forms
class TicketForm(forms.ModelForm):
class Meta:
model = Ticket
fields = ["status"]
widgets = {
"status": forms.Select(choices=Ticket.Status.ACTIVE.choices())
}
i18n (optional)
from django.utils.translation import gettext_lazy as _
class Color(Choices):
RED = _("Red")
GREEN = Choices.Value(_("Green"), css="green")
display accepts Django’s lazy Promise, so translation resolves at render time.
Type hints
- The package ships with type annotations.
- Members are both Enum and
str(subclass), so tools like mypy/pyright see them as string-like values with extra attributes.
Testing
uv sync --dev
uv run pytest -q
With coverage 100% enforced:
uv run pytest --cov
Contributing
- Issues and PRs welcome.
- Please run
ruff check . --fixand keep tests green with 100% coverage.
License
Library is available under the MIT license. The included LICENSE file describes this in detail.
Metadata
Release files for django-better-choices 3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| django_better_choices-3.1.tar.gz | 5.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| django_better_choices-3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 12.2 kB
Release files / django_better_choices-3.1.tar.gz
| Download URL | django_better_choices-3.1.tar.gz |
|---|---|
| Size | 5.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e6ef24e4ad236b4a663377c823b66ffbdaf9f21372c9b75380bd3a955f62e5c7
|
|
BLAKE2b-256 checksum How to use checksums |
1e5ae95b5d07181578a35db0d903fe1e71f3c228893be8c36718eb15ccb5639a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.20
|
Release files / django_better_choices-3.1-py3-none-any.whl
| Download URL | django_better_choices-3.1-py3-none-any.whl |
|---|---|
| Size | 6.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
452b3f83c785ab57f837b35188207cb043a7b6a060d0ffaeca4ba02ff2a2d3e6
|
|
BLAKE2b-256 checksum How to use checksums |
0e4ffbf0b2698361ecb2e4e64a99298a15fcdbae41c3593c960fd99c49c2d70b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.8.20
|