Use External Enums

enum.Enum classes defined externally to your code base or enum classes that otherwise do not inherit from Django’s Enumeration types, are supported. When no choices are present on an enum.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.

EnumField should work with any subclass of enum.Enum.

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


class ExternalChoices(models.Model):

    class TextEnum(str, Enum):

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

    # choices will default to (value, name) pairs
    txt_enum1 = EnumField(TextEnum)

    # you can also override choices
    txt_enum2 = EnumField(
        TextEnum,
        choices=[(en.value, en.name.title()) for en in TextEnum]
    )

The list of choice tuples for each field are:

assert ExternalChoices._meta.get_field('txt_enum1').choices == [
    ('V0', 'VALUE0'),
    ('V1', 'VALUE1'),
    ('V2', 'VALUE2')
]

assert ExternalChoices._meta.get_field('txt_enum2').choices == [
    ('V0', 'Value0'),
    ('V1', 'Value1'),
    ('V2', 'Value2')
]

Warning

One nice feature of Django’s Enumeration types are that they disable enum.auto on enum.Enum fields. enum.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 control over the values is not certain it is a good idea to add integration tests that look for value changes.

Hash Equivalency

Tip

It is a good idea to make sure your enumeration instances are hash equivalent to their primitive values. You can do this simply by inheriting from their primitive value (e.g. class MyEnum(str, Enum):) or by using StrEnum and IntEnum types. Any enumeration defined using Enum Properties will be hash equivalent to its values by default.

EnumField automatically sets the choices tuple on the field. Django has logic in a number of places that handles fields with choices in a special way (e.g. in the admin). For example, the choices may be converted to a dictionary mapping values to labels. The values will be the primitive values of the enumeration not enumeration instances and the current value of the field which may be an enumeration instance will be searched for in the dictionary. This will fail if the enumeration instance is not hash equivalent to its value.

To control the hashing behavior of an object, you must override its __hash__() and __eq__() methods.

For example:

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


class HashEquivalencyExample(Model):
    """
    This example model defines three enum fields. The first uses an enum that
    is not hash equivalent to its values. The second two are.
    """

    class NotHashEq(Enum):
        """
        Enums that inherit only from :class:`~enum.Enum` are not hash equivalent
        to their values by default.
        """

        VALUE1 = "V1"
        VALUE2 = "V2"
        VALUE3 = "V3"

    class HashEq(Enum):
        """
        We can force our Enum to be hash equivalent by overriding the necessary
        dunder methods..
        """

        VALUE1 = "V1"
        VALUE2 = "V2"
        VALUE3 = "V3"

        def __hash__(self):
            return hash(self.value)

        def __eq__(self, value) -> bool:
            if isinstance(value, self.__class__):
                return self.value == value.value
            try:
                return self.value == self.__class__(value).value
            except (ValueError, TypeError):
                return False

    class HashEqStr(str, Enum):  # or StrEnum on py 3.11+
        """
        Or we can inherit from the primitive value type.
        """

        VALUE1 = "V1"
        VALUE2 = "V2"
        VALUE3 = "V3"


    not_hash_eq = EnumField(NotHashEq)
    hash_eq = EnumField(HashEq)
    hash_eq_str = EnumField(HashEqStr)

obj = HashEquivalencyExample.objects.create(
    not_hash_eq=HashEquivalencyExample.NotHashEq.VALUE1,
    hash_eq=HashEquivalencyExample.HashEq.VALUE1,
    hash_eq_str=HashEquivalencyExample.HashEqStr.VALUE1
)

# direct comparisons to values do not work
assert obj.not_hash_eq != "V1"

# unless you have provided __eq__ or inherited from the primitive
assert obj.hash_eq == obj.hash_eq_str == "V1"

# here is the problem that can break some Django internals in rare instances:
assert dict(HashEquivalencyExample._meta.get_field("not_hash_eq").flatchoices) == {
    "V1": "VALUE1",
    "V2": "VALUE2",
    "V3": "VALUE3"
}

try:
    dict(HashEquivalencyExample._meta.get_field("not_hash_eq").flatchoices)[
        HashEquivalencyExample.NotHashEq.VALUE1
    ]
    assert False
except KeyError:
    assert True

# if we've made our enum hash equivalent though, this works:
assert dict(HashEquivalencyExample._meta.get_field("hash_eq").flatchoices)[
    HashEquivalencyExample.HashEq.VALUE1
] == "VALUE1"
assert dict(HashEquivalencyExample._meta.get_field("hash_eq_str").flatchoices)[
    HashEquivalencyExample.HashEqStr.VALUE1
] == "VALUE1"