Usage

EnumField inherits from the appropriate native Django field and sets the correct choice tuple set based on the enumeration type. This means EnumFields are compatible with all modules, utilities and libraries that fields defined with a choice tuple are. For example:

from django.db import models
from django_enum import EnumField

class MyModel(models.Model):

    class TextEnum(models.TextChoices):

        VALUE0 = 'V0', 'Value 0'
        VALUE1 = 'V1', 'Value 1'
        VALUE2 = 'V2', 'Value 2'

    txt_enum = EnumField(TextEnum, null=True, blank=True, default=None)

    txt_choices = models.CharField(
        max_length=2,
        choices=MyModel.TextEnum.choices,
        null=True,
        blank=True,
        default=None
    )

txt_enum and txt_choices fields are equivalent in all ways with the following exceptions:

# txt_enum fields will always be an instance of the TextEnum type, unless
# set to a value that is not part of the enumeration

assert isinstance(MyModel.objects.first().txt_enum, MyModel.TextEnum)
assert not isinstance(MyModel.objects.first().txt_choices, MyModel.TextEnum)

# by default EnumFields are more strict, this is possible:
MyModel.objects.create(
    txt_choices='AA'
)

# but this will throw a ValueError (unless strict=False)
MyModel.objects.create(
    txt_enum='AA'
)

# and this will throw a ValidationError
MyModel(txt_enum='AA').full_clean()

Any ModelForms, DRF serializers and filters will behave the same way with txt_enum and txt_choices. A few types are provided for deeper integration with forms and django-filter but their usage is optional. See Forms and Filtering.

Very rich enumeration fields that encapsulate much more functionality in a simple declarative syntax are possible with EnumField. See enum-properties.

External Enum Types

Enum classes defined externally to your code base or enum classes that otherwise do not inherit from Django’s Choices type, are supported. When no choices are present on an Enum type, EnumField will attempt to use the label member on each enumeration value if it is present, otherwise the labels will be based off the enumeration name. Choices can also be overridden at the EnumField declaration.

In short, EnumField should work with any subclass of Enum.

from enum import Enum
from django.db import models
from django_enum import EnumField

class MyModel(models.Model):

    class TextEnum(str, Enum)

        VALUE0 = 'V0'
        VALUE1 = 'V1'
        VALUE2 = 'V2'

    txt_enum = EnumField(TextEnum)

The above code will produce a choices set like [('V0', 'VALUE0'), ...].

Warning

One nice feature of Django’s Choices type is that it disables auto() on Enum fields. auto() can be dangerous because the values assigned depend on the order of declaration. This means that if the order changes existing database values will no longer align with the enumeration values. When using Enums where control over the values is not certain it is a good idea to add integration tests that look for value changes.

Parameters

All parameters available to the equivalent model field with choices may be set directly in the EnumField instantiation. If not provided EnumField will set choices and max_length automatically.

The following EnumField specific parameters are available:

strict

By default all EnumFields are strict. This means a ValidationError will be thrown anytime full_clean is run on a model and a value is set for the field that can not be coerced to its native Enum type. To allow the field to store values that are not present in the fields Enum type we can pass strict=False.

Non-strict fields that have values outside of the enumeration will be instances of the enumeration where a valid Enum value is present and the plain old data where no Enum type coercion is possible.

class StrictExample(models.Model):

    class EnumType(TextChoices):

        ONE = '1', 'One'
        TWO = '2', 'Two'

    non_strict = EnumField(
        EnumType,
        strict=False,
        # it might be necessary to override max_length also, otherwise
        # max_length will be 1
        max_length=10
    )

obj = StrictExample()

# set to a valid EnumType value
obj.non_strict = '1'
# when accessed will be an EnumType instance
assert obj.non_strict is StrictExample.EnumType.ONE

# we can also store any string less than or equal to length 10
obj.non_strict = 'arbitrary'
obj.full_clean()  # no errors
# when accessed will be a str instance
assert obj.non_strict == 'arbitrary'

coerce

Setting this parameter to False will turn off the automatic conversion to the field’s Enum type while leaving all validation checks in place. It will still be possible to set the field directly as an Enum instance and to filter by Enum instance or any symmetric value:

non_strict = EnumField(
    EnumType,
    strict=False,
    coerce=False,
    # it might be necessary to override max_length also, otherwise
    # max_length will be 1
    max_length=10
)

# set to a valid EnumType value
obj.non_strict = '1'

# when accessed will be the primitive value
assert obj.non_strict == '1'
assert isinstance(obj.non_strict, str)
assert not isinstance(obj.non_strict, StrictExample.EnumType)

enum-properties

TextChoices and IntegerChoices types are provided that extend Django’s native choice types with support for enum-properties. The dependency on enum-properties is optional, so to utilize these classes you must separately install enum-properties:

pip install enum-properties

These choice extensions make possible very rich enumerations that have other values that can be symmetrically mapped back to enumeration values:

from enum_properties import s
from django_enum import TextChoices  # use instead of Django's TextChoices
from django.db import models

class TextChoicesExample(models.Model):

    class Color(TextChoices, s('rgb'), s('hex', case_fold=True)):

        # name   value   label       rgb       hex
        RED     = 'R',   'Red',   (1, 0, 0), 'ff0000'
        GREEN   = 'G',   'Green', (0, 1, 0), '00ff00'
        BLUE    = 'B',   'Blue',  (0, 0, 1), '0000ff'

        # any named s() values in the Enum's inheritance become properties on
        # each value, and the enumeration value may be instantiated from the
        # property's value

    color = EnumField(Color)

instance = TextChoicesExample.objects.create(
    color=TextChoicesExample.Color('FF0000')
)
assert instance.color == TextChoicesExample.Color('Red')
assert instance.color == TextChoicesExample.Color('R')
assert instance.color == TextChoicesExample.Color((1, 0, 0))

# direct comparison to any symmetric value also works
assert instance.color == 'Red'
assert instance.color == 'R'
assert instance.color == (1, 0, 0)

# save by any symmetric value
instance.color = 'FF0000'

# access any enum property right from the model field
assert instance.color.hex == 'ff0000'

# this also works!
assert instance.color == 'ff0000'

# and so does this!
assert instance.color == 'FF0000'

instance.save()

# filtering works by any symmetric value or enum type instance
assert TextChoicesExample.objects.filter(
    color=TextChoicesExample.Color.RED
).first() == instance

assert TextChoicesExample.objects.filter(color=(1, 0, 0)).first() == instance

assert TextChoicesExample.objects.filter(color='FF0000').first() == instance

For a real-world example see Examples.

Forms

An EnumChoiceField type is provided that enables symmetric value resolution and will automatically coerce any set value to the underlying enumeration type. Django’s ModelForms will use this form field type to represent EnumFields by default. For most scenarios this is sufficient. The EnumChoiceField can also be explicitly used. For example, using our TextChoicesExample from above - if color was declared with strict=False, we could add additional choices to our form field like so:

from django_enum import EnumChoiceField

class TextChoicesExampleForm(ModelForm):

    color = EnumChoiceField(
        TextChoicesExample.Color,
        strict=False,
        choices=[
            ('P', 'Purple'),
            ('O', 'Orange'),
        ] + TextChoicesExample.Color.choices
    )

    class Meta:
        model = TextChoicesExample
        fields = '__all__'

# when this form is rendered in a template it will include a selected
# option for the value 'Y' that is not part of our Color enumeration.
# since our field is not strict, we can set it to a value not in our
# enum or choice tuple.
form = TextChoicesExampleForm(
    instance=TextChoicesExample.objects.create(color='Y')
)
<!-- The above will render the following options: -->
<select>

    <!-- our extended choices -->
    <option value='P'>Purple</option>
    <option value='O'>Orange</option>

    <!-- choices from our enum -->
    <option value='R'>Red</option>
    <option value='G'>Green</option>
    <option value='B'>Blue</option>

    <!--
    non-strict fields that have data that is not a valid enum value and is
    not present in the form field's choices tuple will have that value
    rendered as the selected option.
    -->
    <option value='Y' selected>Y</option>
</select>

Django Rest Framework

By default DRF ModelSerializer will use a ChoiceField to represent an EnumField. This works great, but it will not accept symmetric enumeration values. A serializer field EnumField is provided that will. The dependency on DRF is optional so to use the provided serializer field you must install DRF:

pip install djangorestframework
from django_enum.drf import EnumField
from rest_framework import serializers

class ExampleSerializer(serializers.Serializer):

    color = EnumField(TextChoicesExample.Color)

ser = ExampleSerializer(data={'color': (1, 0, 0)})
assert ser.is_valid()

The serializer EnumField accepts any arguments that ChoiceField does. It also accepts the strict parameter which behaves the same way as it does on the model field.

Filtering

As shown above, filtering by any value, enumeration type instance or symmetric value works with Django’s ORM. This is not natively true for automatically generated FilterSets from django-filter. Those filter sets will only be filterable by direct enumeration value by default. An EnumFilter type is provided to enable filtering by symmetric property values, but since the dependency on django-filter is optional, you must first install it:

pip install django-filter
from django_enum import EnumFilter
from django_filters.views import FilterView
from django_filters import FilterSet

class TextChoicesExampleFilterViewSet(FilterView):

    class TextChoicesExampleFilter(FilterSet):

        color = EnumFilter(TextChoicesExample.Color)

        class Meta:
            model = TextChoicesExample
            fields = '__all__'

    filterset_class = TextChoicesExampleFilter
    model = TextChoicesExample

# now filtering by symmetric value in url parameters works:
# e.g.:  /?color=FF0000

An EnumFilterSet type is also provided that uses EnumFilter for EnumFields by default. So the above is also equivalent to:

from django_enum import FilterSet as EnumFilterSet
from django_filters.views import FilterView

class TextChoicesExampleFilterViewSet(FilterView):

    class TextChoicesExampleFilter(EnumFilterSet):
        class Meta:
            model = TextChoicesExample
            fields = '__all__'

    filterset_class = TextChoicesExampleFilter
    model = TextChoicesExample

Migrations

Important

There is one rule for writing custom migration files for EnumFields: Never reference or import your enumeration classes in a migration file, work with the primitive values instead.

The deconstructed EnumFields only include the choices tuple in the migration files. This is because Enum classes may come and go or be altered but the earlier migration files must still work. Simply treat any custom migration routines as if they were operating on a normal model field with choices.

EnumFields in migration files will not resolve the field values to enumeration types. The fields will be the primitive enumeration values as they are with any field with choices.

Performance

The cost to resolve a raw database value into an Enum type object is non-zero. EnumFields may not be appropriate for use cases at the very edge of critical performance targets, but for most scenarios the cost of using EnumFields is negligible.

An effort is made to characterize and monitor the performance penalty of using EnumFields over a Django native field with choices and integration tests ensure performance of future releases will not worsen.

Note

The read performance penalty can be eliminated by setting coerce to False.