Migrating to native Django Choices

Since version 3.0, Django offers native choices enums (mostly equivalent) to the functionality that this library offers. See the django docs for more details.

We provide some automated tooling to facilitate migrating and instructions for possible hurdles.

Generating equivalent native code

For trivial usage where you just define the choices as a constant, there is a management command since version 2.0 to generate the equivalent code. It supports str and int for values types.

  1. Ensure you have djchoices added to your INSTALLED_APPS setting.

  2. Run the command python manage.py generate_native_django_choices

The command essentially discovers all subclasses of DjangoChoices and introspects them to generate the equivalent native choices code. This depends on the classes being imported during Django’s initialization phase. This should be the case for almost all usages, as the choices need to be imported to be picked up by models.

Possible options for the command:

  • --no-wrap-gettext: do not wrap the choice labels in a function call to mark them translatable.

  • --gettext-alias: when wrapping the labels, you can specify the name of the function call/alias to wrap with, e.g. gettext_lazy. It defaults to the common pattern of _. You need to ensure the necessary imports are present in the module.

Public API

  • The choices class attribute behaves the same in native choices.

Migrating non-trivial usage

Django-choices offered some class attributes that need to be updated too when migrating to native choices.

DjangoChoices.labels

This is roughly equivalent to:

dict(zip(Native.names, Native.labels))

Notable differences:

  • for a value like 'option1' without explicit label, Django Choices produces 'option1' as label, while Django produces 'option 1'. A value like 'option_1' results in the same label.

  • It may not play nice with the empty option in native choices.

ChoiceItem.order

The management command emits the generated native choices in the configured order.

If you need access to the order, you can leverage enumerate(Native.values) to loop over tuples of (order, value).

DjangoChoices.values

This is equivalent to:

dict(zip(Native.values, Native.labels))

DjangoChoices.validator

Django has been performing this out of the box on model fields since at least Django 1.3 - you don’t need it.

DjangoChoices.attributes

This is roughly equivalent to:

native = dict(zip(Native.values, Native.names))

Remarks:

  • It may not play nice with the empty option in native choices.

DjangoChoices.get_choice

There is no direct equivalent, however you can access the enum instance and look up properties:

an_enum_value = Native[Native.some_value]
print(an_enum_value.value)
print(an_enum_value.label)

DjangoChoices.get_order_expression

There is no equivalent, but you should easily be able to add this as your own class method/mixin:

from django.db.models import Case, IntegerField, Value, When

@classmethod
def get_order_expression(cls, field_name):
    whens = []
    for order, value in enumerate(cls.values()):
        whens.append(
            When(**{field_name: value, "then": Value(order)})
        )
    return Case(*whens, output_field=IntegerField())

Custom attributes

It’s recommended to keep a separate dictionary with a mapping of choice values to the additional attributes. You could consider dataclasses to model this too.