Skip to main content

Better choices library for Django web framework

Project description

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.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

django_better_choices-3.0.tar.gz (5.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

django_better_choices-3.0-py3-none-any.whl (6.3 kB view details)

Uploaded Python 3

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

Hashes for django_better_choices-3.0.tar.gz
Algorithm Hash digest
SHA256 a57aa59ff86dd59becff1e749ef17c7ae5dffffdc39ff9d6cce99c5cec0a9f2f
MD5 737ac624ba9b66497b10e4cd7931c679
BLAKE2b-256 c291f1c85a0a854d16fab15e93e42b82c21067cd007c3eb87141906a3aa56a8c

See more details on using hashes here.

File details

Details for the file django_better_choices-3.0-py3-none-any.whl.

File metadata

File hashes

Hashes for django_better_choices-3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 d6ddfa583b4531bb899739686a3d5be1ba425f034803547ea804891689dccbd3
MD5 68c32e23186883e9a51246a3e50ee819
BLAKE2b-256 fa29f51afe4478c69c91f4bdef291b9fc74b2e6f231c4e0ec9cd73868e94572d

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page