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:
- Type Specificity
Any value that matches a property value without a type coercion will take precedence over values that match after type coercion.
- Left to right.
Any value with a smaller tuple index will override any value with a larger tuple index
- 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
from enum_properties import EnumProperties
from enum import nonmember, member
class MyEnum(EnumProperties):
@nonmember
class Type1:
pass
@nonmember
class Type2:
pass
@nonmember
class Type3:
pass
label: str
VALUE1 = member(Type1), 'label1'
VALUE2 = member(Type2), 'label2'
VALUE3 = member(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
BEETLEandDOGin the above example are instances of theCreatureDataMixindataclass. 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
EnumPropertiesclass will suffice.dataclassesare 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
valueattribute 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()ors()type – used directly, which lets you configure symmetry andcase_foldoptions.
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'