While grokking the source code of http.HTTPStatus
module, I came across this technique to
add extra 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
Recent posts
- SSH saga
- Injecting Pytest fixtures without cluttering test signatures
- Explicit method overriding with @typing.override
- Quicker startup with module-level __getattr__
- Docker mount revisited
- Topological sort
- Writing a circuit breaker in Go
- Discovering direnv
- Notes on building event-driven systems
- Bash namerefs for dynamic variable referencing