Skip to main content

Django Better Choices

PyPI PyPI - Python Version Build Status codecov License

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 . --fix and 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)

Source distribution for django-better-choices 3.1
File Size Uploaded
django_better_choices-3.1.tar.gz 5.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for django-better-choices 3.1
File Interpreter ABI Platform
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

Release history Release notifications | RSS feed

This release

3.1 This release

2 release files

3.0

2 release files

1.18

2 release files

1.17

2 release files

1.16

2 release files

1.15

2 release files

1.14

2 release files

1.13

2 release files

1.12

2 release files

1.11

2 release files

1.10

2 release files

1.9

2 release files

1.8

2 release files

1.7

2 release files

1.6

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page