Python enum with multiple attributes

Before Python 3.4 and the addition of the excellent enum module, a good choice would have been to use a namedtuple:

from collections import namedtuple

Item = namedtuple('abitem', ['a', 'b'])

class Items:
    GREEN = Item('a', 'b')
    BLUE = Item('c', 'd')

These days, any supported version of Python has enum, so please use that module. It gives you a lot more control over how each enum value is produced.

If you give each item a tuple of values, then these are passed to the __init__ method as separate (positional) arguments, which lets you set additional attributes on the enum value:

from enum import Enum

class Items(Enum):
    GREEN = ('a', 'b')
    BLUE = ('c', 'd')

    def __init__(self, a, b):
        self.a = a
        self.b = b

This produces enum entries whose value is the tuple assigned to each name, as well as two attributes a and b:

>>> Items.GREEN, Items.BLUE
(<Items.GREEN: ('a', 'b')>, <Items.BLUE: ('c', 'd')>)
>>> Items.BLUE.a
'c'
>>> Items.BLUE.b
'd'
>>> Items(('a', 'b'))
<Items.GREEN: ('a', 'b')>

Note that you can look up each enum value by passing in the same tuple again.

If the first item should represent the value of each enum entry, use a __new__ method to set _value_:

from enum import Enum

class Items(Enum):
    GREEN = ('a', 'b')
    BLUE = ('c', 'd')

    def __new__(cls, a, b):
        entry = object.__new__(cls) 
        entry.a = entry._value_ = a  # set the value, and the extra attribute
        entry.b = b
        return entry

    def __repr__(self):
        return f'<{type(self).__name__}.{self.name}: ({self.a!r}, {self.b!r})>'

I added a custom __repr__ as well, the default only includes self._value_. Now the value of each entry is defined by the first item in the tuple, and can be used to look up the enum entry:

>>> Items.GREEN, Items.BLUE
(<Items.GREEN: ('a', 'b')>, <Items.BLUE: ('c', 'd')>)
>>> Items.BLUE.a
'c'
>>> Items.BLUE.b
'd'
>>> Items('a')
<Items.GREEN: ('a', 'b')>

See the section on __init__ vs. __new__ in the documentation for further options.

While grokking the source code of http.HTTPStatus module, I came across this technique to add additional attributes to the values of enum members. Now, to understand what do I mean by adding attributes, let's consider the following example:

# src.py
from __future__ import annotations

from enum import Enum


class Color(str, Enum):
    RED = "Red"
    GREEN = "Green"
    BLUE = "Blue"

Here, I've inherited from str to ensure that the values of the enum members are strings. This class can be used as follows:

# src.py
...

# Print individual members.
print(f"{Color.RED=}")

# Print name as a string.
print(f"{Color.GREEN.name=}")

# Print value.
print(f"{Color.BLUE.value=}")

Running the script will print:

Color.RED=<Color.RED: 'Red'>
Color.GREEN.name='GREEN'
Color.BLUE.value='Blue'

While this works but it's evident that you can only assign a single value to an enum member. How'd you rewrite this if you needed multiple values attached to a single enum member?

Suppose, in the above case, along with the color title, you also need to save the hex codes and short descriptions of the colors. One way you can achieve this is via the assignment of an immutable container as the value of an enum member:

# src.py
from __future__ import annotations

from enum import Enum


class Color(Enum):
    RED = ("Red", "#ff0000", "Ruby Red")
    GREEN = ("Green", "#00ff00", "Guava Green")
    BLUE = ("Blue", "#0000ff", "Baby Blue")

Here, I'm using a tuple to contain the title, hex code, and description of the Color members. This gets awkward whenever you'll need to access the individual elements inside the tuple. You'll have to use hardcoded indexes to access the elements of the tuple. This is how you'll probably use it:

...

for c in Color:
    print(
        f"title={c.value[0]}, hex_code={c.value[1]}, description={c.value[2]}"
    )

It prints:

title=Red, hex_code=#ff0000, description=Ruby Red
title=Green, hex_code=#00ff00, description=Guava Green
title=Blue, hex_code=#0000ff, description=Baby Blue

Hardcoding indexes in such a manner is fragile and will break if you drop a new value in the middle of the tuple assigned to an enum member. Also, it's hard to reason through logic when you've to keep the semantic meanings of the index positions in your working memory. A better thing to do is to rewrite the enum in a way that'll allow you to access different elements of the member values by their attribute names. Let's do it:

from __future__ import annotations

from enum import Enum


class Color(str, Enum):
    # Declaring the additional attributes here keeps mypy happy.
    hex_code: str
    description: str

    def __new__(
        cls, title: str, hex_code: str = "", description: str = ""
    ) -> Color:

        obj = str.__new__(cls, title)
        obj._value_ = title

        obj.hex_code = hex_code
        obj.description = description
        return obj

    RED = ("Red", "#ff0000", "Ruby Red")
    GREEN = ("Green", "#00ff00", "Guava Green")
    BLUE = ("Blue", "#0000ff", "Baby Blue")

Here, I overrode the __new__ method of the class Color. Method __new__ is a special class method that you don't need to decorate with the @classmethod decorator. It gets executed during the creation of the Color object; before the __init__ method. Other than the first argument cls, you can define the __new__ method with any number of arbitrarily named arguments.

In this case, the value of each member of Color will have three elements—title, hex_code, and description. So, I defined the __new__ method to accept those arguments. In the following line, the str class was initialized via obj = str.__new__(cls, title) and then title was assigned to the newly created string object via obj._value_=title. This line is crucial; without it, the enum won't operate at all. This assignment makes sure that the Enum.member.value will return a string value.

In the next two lines, hex_code and description were attached to the member values via the obj.hex_code=hexcode and obj.description=description statements respectively.

Now, you'll be able to use this enum without any hardcoded shenanigans:

...

# Access the elements of the values of the members by names.
print(f"{Color.RED.value=}")
print(f"{Color.BLUE.hex_code=}")
print(f"{Color.GREEN.description=}")

# Iterate through all the memebers.
for c in Color:
    print(
        f"title={c.value}, hex_code={c.hex_code}, description={c.description}"
    )

This will print:

Color.RED.value='Red'
Color.BLUE.hex_code='#0000ff'
Color.GREEN.description='Guava Green'
title=Red, hex_code=#ff0000, description=Ruby Red
title=Green, hex_code=#00ff00, description=Guava Green
title=Blue, hex_code=#0000ff, description=Baby Blue

References¶

  • http.HTTPStatus

Can enum have multiple values Python?

Enums can't have multiple value per name.

What is Auto () in Python?

auto() method, we can get the assigned integer value automatically by just using enum. auto() method. Syntax : enum.auto() Automatically assign the integer value to the values of enum class attributes.

What are enumerations in Python?

An enumeration is a set of symbolic names (members) bound to unique, constant values. Within an enumeration, the members can be compared by identity, and the enumeration itself can be iterated over.

Should enums be capitalized Python?

Notice that enumerations are not normal Python classes, even though they are defined in a similar way. As enumerations are meant to represent constant values, it is advisable to uppercase the enum member names.