How To

Add Properties to an Enum

To add properties to an enumeration you must inherit from EnumProperties or IntEnumProperties instead of enum.Enum and enum.IntEnum, list property values in a tuple with each enumeration value and let EnumProperties know that your properties exist and what their names are by adding type hints to the Enum class definition before the enumeration values. The type hints must be in the same order as property values are listed in the value tuples:

Warning

A ValueError will be thrown if the length of any value tuple does not match the number of expected properties. If a given enumeration value does not have a property, None should be used.

For example:

import typing as t
from enum_properties import EnumProperties
from enum import auto


class Color(EnumProperties):

    rgb: t.Tuple[int, int, int]
    hex: str

    # name   value      rgb       hex
    RED    = auto(), (1, 0, 0), 'ff0000'
    GREEN  = auto(), (0, 1, 0), '00ff00'
    BLUE   = auto(), (0, 0, 1), '0000ff'


# The property values are accessible by name on the enumeration values:
assert Color.RED.hex == 'ff0000'

Tip

The property type hints must be specified before the enumeration values to become properties. If you would like a type hint on your enumeration that are not properties, you may specify the hint after the value definitions.

Use a metaclass instead

EnumProperties inherits from enum and all other standard python enumeration functionality will work. The EnumProperties base class is equivalent to:

from enum_properties import EnumPropertiesMeta
from enum import Enum, auto


class Color(Enum, metaclass=EnumPropertiesMeta):

Get Enums from their properties

For some enumerations it will make sense to be able to fetch an enumeration value instance from one of the property values. This is called property symmetry. To mark a property as symmetric, annotate your type hint with Symmetric:

import typing as t
from enum_properties import EnumProperties, Symmetric
from enum import auto


class Color(EnumProperties):

    rgb: t.Annotated[t.Tuple[int, int, int], Symmetric()]
    hex: t.Annotated[str, Symmetric(case_fold=True)]

    # name   value      rgb       hex
    RED    = auto(), (1, 0, 0), '0xff0000'
    GREEN  = auto(), (0, 1, 0), '0x00ff00'
    BLUE   = auto(), (0, 0, 1), '0x0000ff'


assert Color.RED is Color((1, 0, 0)) is Color('0xFF0000') is Color('0xff0000')


# str(hex(16711680)) == '0xff0000'
assert Color.RED is Color(hex(16711680)) == hex(16711680)
assert Color.RED == (1, 0, 0)
assert Color.RED != (0, 1, 0)
assert Color.RED == '0xFF0000'

Tip

Symmetric string properties are by default case sensitive. To mark a property as case insensitive, use the case_fold=True parameter on the Symmetric dataclass.

case_fold will more than just make matching case insensitive. It will store the string using the unicode standard Normalization Form Compatibility Decomposition (NFKD) algorithm. This breaks down characters into their canonical components. For example, accented characters like “é” are decomposed to “e”. This is particularly useful when you want to compare strings or search text in a way that ignores differences in case and accent/diacritic representations.

For futher reading, here’s more than you ever wanted to know about unicode.

Tip

By default, none values for symmetric properties will not be symmetric. To change this behavior pass: match_none=True to Symmetric.

Warning

Any object may be a property value, but symmetric property values must be hashable. A ValueError will be thrown if they are not.

An exception to this is that symmetric property values may be a list or set of hashable values. Each value in the list will be symmetric to the enumeration value. Tuples are hashable and are treated as singular property values. See the AddressRoute example in Tutorial.

SymmetricMixin tries very hard to resolve enumeration values from objects. Type coercion to all potential value types will be attempted before giving up. For instance, if we have a hex object that is coercible to a string hex value we could instantiate our Color enumeration from it and perform equality comparisons:

# str(hex(16711680)) == '0xff0000'
assert Color.RED is Color(hex(16711680)) == hex(16711680)
assert Color.RED == (1, 0, 0)
assert Color.RED != (0, 1, 0)
assert Color.RED == '0xFF0000'

Warning

Using symmetric properties with @verify(UNIQUE) will raise an error:

import typing as t
from enum_properties import EnumProperties, Symmetric
from enum import verify, UNIQUE


@verify(UNIQUE)
class Color(EnumProperties):

    label: t.Annotated[str, Symmetric()]

    RED = 1, 'red'
    GREEN = 2, 'green'
    BLUE = 3, 'blue'

# ValueError: aliases found in <enum 'Color'>: blue -> BLUE,
# green -> GREEN, red -> RED

Use a metaclass instead

Symmetric property support is added through the SymmetricMixin class which is included in the EnumProperties base class. If you are using the metaclass you must also inherit from SymmetricMixin:

from enum_properties import EnumPropertiesMeta, SymmetricMixin, Symmetric
from enum import Enum, auto


class Color(SymmetricMixin, Enum, metaclass=EnumPropertiesMeta):

Handle Symmetric Overloads

Symmetric properties need not be unique. Resolution by value is deterministic based on the following priority order:

  1. Type Specificity

    Any value that matches a property value without a type coercion will take precedence over values that match after type coercion.

  2. Left to right.

    Any value with a smaller tuple index will override any value with a larger tuple index

  3. Nested left to right.

    Any value in a list or set of symmetric values will override values with larger indexes in corresponding property values.

import typing as t
from enum_properties import IntEnumProperties, Symmetric


class PriorityEx(IntEnumProperties):

    prop1: t.Annotated[str, Symmetric()]
    prop2: t.Annotated[t.List[int | str], Symmetric(case_fold=True)]

    # <-------- Higher Precedence
    # name  value   prop1     prop2    #  ^
    ONE     = 0,     '1',    [3, 4]    #  |
    TWO     = 1,     '2',    [3, '4']  #  Higher
    THREE   = 2,     '3',    [3, 4]    #  Precedence


assert PriorityEx(0)   is PriorityEx.ONE   # order left to right
assert PriorityEx('1') is PriorityEx.ONE   # type specificity
assert PriorityEx(3)   is PriorityEx.ONE   # type specificity/order
assert PriorityEx('3') is PriorityEx.THREE # type specificity
assert PriorityEx(4)   is PriorityEx.ONE   # order left to right
assert PriorityEx('4') is PriorityEx.TWO   # type specificity

Mark name as Symmetric

When extending from enum.Enum or other enumeration base classes, some builtin properties are available. name is available on all standard enum.Enum classes. By default EnumProperties will make name case sensitive symmetric. To override this behavior, you may add a type hint for name before your added property type hints. For example to make name case insensitive we might:

import typing as t
from enum import auto
from enum_properties import EnumProperties, Symmetric


class Color(EnumProperties):

    name: t.Annotated[str, Symmetric(case_fold=True)]
    rgb: t.Annotated[t.Tuple[int, int, int], Symmetric()]
    hex: t.Annotated[str, Symmetric(case_fold=True)]

    # name   value      rgb       hex
    RED    = auto(), (1, 0, 0), 'ff0000'
    GREEN  = auto(), (0, 1, 0), '00ff00'
    BLUE   = auto(), (0, 0, 1), '0000ff'


# now we can do this:
assert Color('red') is Color.RED

Mark @properties as Symmetric

The symmetric() decorator may be used to mark methods as symmetric. Plain functions are automatically wrapped as properties, so @property is not required. For example:

import typing as t
from enum import auto
from enum_properties import EnumProperties, symmetric


class Color(EnumProperties):

    rgb: t.Tuple[int, int, int]
    hex: str

    # name   value      rgb       hex
    RED    = auto(), (1, 0, 0), 'ff0000'
    GREEN  = auto(), (0, 1, 0), '00ff00'
    BLUE   = auto(), (0, 0, 1), '0000ff'

    @symmetric()
    def integer(self) -> int:
        return int(self.hex, 16)

    @symmetric()
    def binary(self) -> str:
        return bin(self.integer)[2:]


# now we can do this:
assert Color(Color.RED.binary) is Color.RED
assert Color(Color.RED.integer) is Color.RED

Specializing Member Functions

Provide specialized implementations of member functions using the specialize decorator. For example:

from enum_properties import EnumProperties, specialize


class SpecializedEnum(EnumProperties):

    ONE   = 1
    TWO   = 2
    THREE = 3

    @specialize(ONE)
    def method(self):
        return 'method_one()'

    @specialize(TWO)
    def method(self):
        return 'method_two()'

    @specialize(THREE)
    def method(self):
        return 'method_three()'


assert SpecializedEnum.ONE.method() == 'method_one()'
assert SpecializedEnum.TWO.method() == 'method_two()'
assert SpecializedEnum.THREE.method() == 'method_three()'

The specialize() decorator works on @classmethods and @staticmethods as well, but it must be the outermost decorator.

The undecorated method will apply to all members that lack a specialization:

from enum_properties import EnumProperties, specialize


class SpecializedEnum(EnumProperties):

    ONE   = 1
    TWO   = 2
    THREE = 3

    def method(self):
        return 'generic()'

    @specialize(THREE)
    def method(self):
        return 'method_three()'


assert SpecializedEnum.ONE.method() == 'generic()'
assert SpecializedEnum.TWO.method() == 'generic()'
assert SpecializedEnum.THREE.method() == 'method_three()'

If no undecorated method or specialization for a value is found that value will lack the method.

from enum_properties import EnumProperties, specialize


class SpecializedEnum(EnumProperties):

    ONE   = 1
    TWO   = 2
    THREE = 3

    @specialize(THREE)
    def method(self):
        return 'method_three()'

assert not hasattr(SpecializedEnum.ONE, 'method')
assert not hasattr(SpecializedEnum.TWO, 'method')
assert SpecializedEnum.THREE.method() == 'method_three()'

specialize() will also accept a list so that multiple enumeration values can share the same specialization.

from enum_properties import EnumProperties, specialize


class SpecializedEnum(EnumProperties):

    ONE   = 1
    TWO   = 2
    THREE = 3

    @specialize(TWO, THREE)
    def method(self):
        return 'shared()'

assert not hasattr(SpecializedEnum.ONE, 'method')
assert SpecializedEnum.TWO.method() == 'shared()'
assert SpecializedEnum.THREE.method() == 'shared()'

Flags

enum.IntFlag and enum.Flag types that support properties are also provided by the IntFlagProperties and FlagProperties classes. For example:

import typing as t
from enum_properties import IntFlagProperties, Symmetric

class Perm(IntFlagProperties):

    label: t.Annotated[str, Symmetric(case_fold=True)]

    R = 1, 'read'
    W = 2, 'write'
    X = 4, 'execute'
    RWX = 7, 'all'


# properties for combined flags, that are not listed will not exist
assert not hasattr((Perm.R | Perm.W), "label")

# but combined flags can be specified and given properties
assert (Perm.R | Perm.W | Perm.X) is Perm.RWX
assert (Perm.R | Perm.W | Perm.X).label == 'all'

# list the active flags:
assert (Perm.R | Perm.W).flagged == [Perm.R, Perm.W]
assert (Perm.R | Perm.W | Perm.X).flagged == [Perm.R, Perm.W, Perm.X]

Flag enumerations can also be created from iterables and generators containing values or symmetric values.

assert Perm([Perm.R, Perm.W, Perm.X]) is Perm.RWX
assert Perm({'read', 'write', 'execute'}) is Perm.RWX
assert Perm(perm for perm in (1, 'write', Perm.X)) is Perm.RWX

# iterate through active flags
assert [perm for perm in Perm.RWX] == [Perm.R, Perm.W, Perm.X]

# flagged property returns list of flags
assert (Perm.R | Perm.W).flagged == [Perm.R, Perm.W]

# instantiate a Flag off an empty iterable
assert Perm(0) == Perm([])

# check number of active flags:
assert len(Perm(0)) == 0
assert len(Perm.RWX) == 3
assert len(Perm.R | Perm.X) == 2
assert len(Perm.R & Perm.X) == 0

Note

Iterable instantiation on flags is added using the DecomposeMixin. To create a flag enumeration without the iterable extensions we can simply declare it manually without the mixin:

import typing as t
import enum
from enum_properties import EnumPropertiesMeta, SymmetricMixin, Symmetric


class Perm(
    SymmetricMixin,
    enum.IntFlag,
    metaclass=EnumPropertiesMeta
):

As of Python 3.11 boundary values are supported on flags. Boundary specifiers must be supplied as named arguments:

import typing as t
from enum_properties import IntFlagProperties, Symmetric
from enum import STRICT


class Perm(IntFlagProperties, boundary=STRICT):

    label: t.Annotated[str, Symmetric(case_fold=True)]

    R = 1, 'read'
    W = 2, 'write'
    X = 4, 'execute'
    RWX = 7, 'all'


Perm(8)  # raises ValueError

Use Nested Classes as Enums

You can use nested classes as enumeration values. The tricky part is keeping them from becoming values themselves.

On enums that inherit from enum.Enum in python < 3.13 nested classes become enumeration values because types may be values and a quirk of Python makes it difficult to determine if a type on a class is declared as a nested class during __new__. For enums with properties we can distinguish declared classes because values must be tuples.

Note that on 3.13 and above you must use the nonmember/member decorators. Also note that the position of label is important.

from enum_properties import EnumProperties


class MyEnum(EnumProperties):

    label: str

    class Type1:
        pass

    class Type2:
        pass

    class Type3:
        pass

    VALUE1 = Type1, 'label1'
    VALUE2 = Type2, 'label2'
    VALUE3 = Type3, 'label3'


# only the expected values become enumeration values
assert MyEnum.Type1 == MyEnum.VALUE1
assert MyEnum.Type2 == MyEnum.VALUE2
assert MyEnum.Type3 == MyEnum.VALUE3
assert len(MyEnum) == 3, len(MyEnum)

# nested classes behave as expected
assert MyEnum.Type1().__class__ is MyEnum.Type1
assert MyEnum.Type2().__class__ is MyEnum.Type2
assert MyEnum.Type3().__class__ is MyEnum.Type3

What about dataclass Enums?

As of Python 3.12, Enum values can be dataclasses. At first glance this enables some behavior that is similar to adding properties. For example:

from dataclasses import dataclass, field
from enum import Enum


@dataclass
class CreatureDataMixin:
    size: str
    legs: int
    tail: bool = field(repr=False, default=True)


class Creature(CreatureDataMixin, Enum):
    BEETLE = 'small', 6
    DOG = 'medium', 4


# you can now access the dataclass fields on the enumeration values
# as with enum properties:
assert Creature.BEETLE.size == 'small'
assert Creature.BEETLE.legs == 6
assert Creature.BEETLE.tail is True

We still recommend EnumProperties as the preferred way to add additional attributes to a Python enumeration for the following reasons:

  • The value of BEETLE and DOG in the above example are instances of the CreatureDataMixin dataclass. This can complicate interfacing with other systems (like databases) where it is more natural for the enumeration value to be a small primitive type like a character or integer.

  • The dataclass method requires two classes where a single EnumProperties class will suffice.

  • dataclasses are not hashable by default which can complicate equality testing and marshalling external data into enumeration values.

  • Many code bases that use duck typing and that work with Enums expect the value attribute to be a a plain old data type and therefore serializable.

Note

EnumProperties also integrates with Enum’s dataclass support! For example we can add a symmetric property to the Creature enumeration like so (note the tuple encapsulating the dataclass fields):

import typing as t
from dataclasses import dataclass, field
from enum_properties import EnumProperties, Symmetric


@dataclass
class CreatureDataMixin:
    size: str
    legs: int
    tail: bool = field(repr=False, default=True)


class Creature(CreatureDataMixin, EnumProperties):

    kingdom: t.Annotated[str, Symmetric()]

    BEETLE = ('small', 6, False), 'insect'
    DOG = ('medium', 4), 'animal'


# you can now access the dataclass fields on the enumeration values
# as with enum properties:
assert Creature.BEETLE.size == 'small'
assert Creature.BEETLE.legs == 6
assert Creature.BEETLE.tail is False
assert Creature.BEETLE.kingdom == 'insect'

# adding symmetric properties onto a dataclass enum can help with
# marshalling external data into the enum classes!
assert Creature('insect') is Creature.BEETLE

Get members and aliases

Symmetric properties are added to the __members__ attribute, and alias members do not appear in _member_names_. To get a list of first class members and aliases use __first_class_members__. This class member may also be overridden if you wish to customize this behavior for users.

import typing as t
from enum_properties import EnumProperties, Symmetric


class MyEnum(EnumProperties):

    label: t.Annotated[str, Symmetric()]

    A = 1, "a"
    B = 2, "b"
    C = 3, "c"
    ALIAS_TO_A = A, "a"


# __first_class_members__ contains members and aliases
assert MyEnum.__first_class_members__ == ["A", "B", "C", "ALIAS_TO_A"]

# __members__ contains all members, including aliases and symmetric aliases
assert set(MyEnum.__members__.keys()) == {"A", "B", "C", "ALIAS_TO_A", "a", "b", "c"}

# iterating contains only non-alias members
assert list(MyEnum) == [MyEnum.A, MyEnum.B, MyEnum.C]

Define hash equivalent enums

The enum.Enum types that inherit from primitive types int and str are hash equivalent to their primitive types. This means that they can be used interchangeably in collections that use hashing:

from enum import IntEnum


class MyIntEnum(IntEnum):

    ONE = 1
    TWO = 2
    THREE = 3


assert {1: 'Found me!'}[MyIntEnum.ONE] == 'Found me!'

IntEnumProperties, StrEnumProperties and IntFlagProperties also honor this hash equivalency. When defining your own symmetric enumeration types if you want to keep hash equivalency to the value type you will you will have to implement this yourself. For example, if you wanted your color enumeration to also be an rgb tuple:

import typing as t
from enum_properties import EnumPropertiesMeta, SymmetricMixin, Symmetric
from enum import Enum


class Color(
    SymmetricMixin,
    tuple,
    Enum,
    metaclass=EnumPropertiesMeta
):
    hex: t.Annotated[str, Symmetric(case_fold=True)]

    # name   value (rgb)    hex
    RED    = (1, 0, 0), '0xff0000'
    GREEN  = (0, 1, 0), '0x00ff00'
    BLUE   = (0, 0, 1), '0x0000ff'

    def __hash__(self):  # you must add this!
        return tuple.__hash__(self)


assert {(1, 0, 0): 'Found me!'}[Color.RED] == 'Found me!'

Use the Functional (Dynamic) API

Python’s standard enum.Enum supports a functional API that creates enumeration classes dynamically at runtime. EnumProperties extends this with a properties keyword argument that names the extra fields packed into each member’s value tuple.

Each entry in properties can be:

  • A string – creates a plain (non-symmetric) property with that name, equivalent to p().

  • A p() or s() type – used directly, which lets you configure symmetry and case_fold options.

import typing as t
from enum_properties import EnumProperties, IntEnumProperties, FlagProperties, p, s


# The functional API lets you build enumeration classes dynamically at runtime.
# Pass a ``properties`` argument with the member definitions to name the properties.
# String entries become plain (non-symmetric) properties; p() and s() types give
# more control, including symmetry.

Color = EnumProperties(
    'Color',
    {
        'RED':   (1, 'Roja',  'ff0000'),
        'GREEN': (2, 'Verde', '00ff00'),
        'BLUE':  (3, 'Azul',  '0000ff'),
    },
    properties=('spanish', s('hex', case_fold=True)),
)

assert Color.RED.spanish == 'Roja'
assert Color.RED.hex == 'ff0000'

# hex is symmetric – look up by hex string (case-insensitive)
assert Color('ff0000') is Color.RED
assert Color('FF0000') is Color.RED
assert Color('00ff00') is Color.GREEN

FlagProperties and IntFlagProperties are also supported:


Perm = FlagProperties(
    'Perm',
    {
        'R':   (1, 'read'),
        'W':   (2, 'write'),
        'X':   (4, 'execute'),
        'RWX': (7, 'all'),
    },
    properties=(s('label', case_fold=True),),
)

assert Perm.R.label == 'read'
assert Perm('READ') is Perm.R
assert (Perm.R | Perm.W | Perm.X) is Perm.RWX
assert Perm.RWX.flagged == [Perm.R, Perm.W, Perm.X]

Use the legacy (1.x) API

The legacy (1.x) way of specifying properties using p() and s() value inheritance is still supported. If any properties are defined this way it will take precedence over type hinting and the type hints will not be interpreted as properties. For example:

from enum_properties import EnumProperties, p, s
from enum import auto


# we use p and s values to define properties in the order they appear in the value tuple
class Color(EnumProperties, p('rgb'), s('hex')):

    extra: int  # this does not become a property

    # non-value tuple properties are marked symmetric using the _symmetric_builtins_
    # class attribute
    _symmetric_builtins_ = [s("name", case_fold=True), "binary"]

    # name   value      rgb       hex
    RED    = auto(), (1, 0, 0), 'ff0000'
    GREEN  = auto(), (0, 1, 0), '00ff00'
    BLUE   = auto(), (0, 0, 1), '0000ff'

    @property
    def binary(self) -> str:
        return bin(int(self.hex, 16))[2:]


assert Color('red') is Color.RED
assert Color('111111110000000000000000') is Color.RED

assert Color.RED.rgb == (1, 0, 0)
assert Color.RED.hex == 'ff0000'
assert Color.RED.binary == '111111110000000000000000'