Better choices library for Django web framework
Project description
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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file django_better_choices-3.0.tar.gz.
File metadata
- Download URL: django_better_choices-3.0.tar.gz
- Upload date:
- Size: 5.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.20
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a57aa59ff86dd59becff1e749ef17c7ae5dffffdc39ff9d6cce99c5cec0a9f2f
|
|
| MD5 |
737ac624ba9b66497b10e4cd7931c679
|
|
| BLAKE2b-256 |
c291f1c85a0a854d16fab15e93e42b82c21067cd007c3eb87141906a3aa56a8c
|
File details
Details for the file django_better_choices-3.0-py3-none-any.whl.
File metadata
- Download URL: django_better_choices-3.0-py3-none-any.whl
- Upload date:
- Size: 6.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: uv/0.8.20
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d6ddfa583b4531bb899739686a3d5be1ba425f034803547ea804891689dccbd3
|
|
| MD5 |
68c32e23186883e9a51246a3e50ee819
|
|
| BLAKE2b-256 |
fa29f51afe4478c69c91f4bdef291b9fc74b2e6f231c4e0ec9cd73868e94572d
|