Meet Enums: The Hidden Power of Python

A deep dive into enums — understanding why it’s important to use enums.

Sergio Daniel Cortez Chavez
Python in Plain English

--

Photo by Steve Johnson on Unsplash

In this post, I going to explain the full power of enums, why it’s important to use them, some alternatives that are introduced with the help of type annotations, and many advanced uses that make writing enums faster and more useful.

Definition

An enumeration is a set of symbolic names (members) bound to unique, constant values

Python documentation: Enum definition

The main purpose of the enum is to aggregate a group of constant values under a single namespace. This makes it more simple to know all the possible values that can take one variable.

Exists many ways to define an enum, but the more simple way is inheriting from the enum.Enum class.

The new State clarified the possible values of the state of some system, and constraint the value to two possible values (OFF, ON), but now, how do you get access to the ON and OFF member?

You have three options:

  1. Acces like a class property.
State.OFF  # <State.OFF: 0>
State.ON # <State.ON: 1>

2. Access with the member value. The member value is the value that is assigned to each constant under the enum definition.

State(1)  # <State.ON: 1>

Note: This means that the State is a callable object.

3. You can access it with the member name as str .

State['ON']  # <State.ON: 1>

Note: This means that the State implements the __getitem__ method. If the member does not exist, a KeyError is raised.

A similar option is to use the getattr function.

getattr(State, 'ON')

Finally, the more obvious way to get a member of the enum is through the class property (State.ON), but the other two options can be helpful in situations where the option comes from the user input at runtime.

Member object

Once you have the member, you can see many important things about it:

  1. The members implement a custom __str__ and __repr__ method.
print(State.ON)  # State.ON
print(repr(State.ON)) # <State.ON: 1>

2. You can compare the enum members by the identity operator (is):

State.ON is State.ON  # True

The enum members follow the singleton pattern. In other words, no matter how many times you access the member ON , this will always be the same object, with the same memory address, which is why it is possible to use the identity operator.

3. In general, the members has two principal attributes:

# ...
# class State(enum.Enum)
# ON = 1
# ...
State.ON.name # 'ON'
State.ON.value # 1

4. Finally, the members behave as instances of the enum class.

isinstance(State.ON, State)  # True

Enum values are not only integers values.

So far, we have only used the enums in the more basic way, we have declared an enum with two members, and each member has an integer value, but the enum members are not constrained to use integer values, they can be of any type, for example:

Although Python allows you to define a mix of types in the member values, in more practical cases, all the members share the same type, for example, integers or strings.

Continuing with the first example, you can extract the list of the members.

list(State)  # [<State.OFF: 0>, <State.ON: 1>]

Or iterate over them:

for member in State:
print(member)
# State.OFF
# State ON

Note: This means that implements the Iterable protocol. One important point is that the order of iteration is the same that the definition.

This technique has one downside related to a special feature named alias members. An alias member is an enum member that has the same value as another member, for example:

from enum import Enumclass State(Enum):
ON = 1
OFF = 2
OFF_ALIAS = 2

In this example, OFF_ALIAS is an alias of the OFF variable, because both members share the same value (2). Now, When you iterate over this enum, you are going to get only two members, ignoring the last one (the alias member).

list(State)  # [<State.OFF: 0>, <State.ON: 1>]

To avoid this problem, you can get the complete list of members with the __members__ property.

State.__members__  # [<State.OFF: 0>, <State.ON: 1>, <State: OFF_ALIAS: 0>]

If you need to prevent the existence of an alias member, you can constraint the enum to only allow unique values with the unique decorator. In this case, if an alias member is inserted, a ValueError exception will be raised.

from enum import unique, Enum@unique
class State(Enum):
ON = 1
OFF = 2
OFF_ALIAS = 2 # ValueError: duplicate values found in <enum 'State'>: OFF_ALIAS -> OFF

In the same way, if the member value doesn't really have a special meaning in the context, and you are only aggregated some constant's value on the same namespace, you can use the auto function as the member value.

from enum import Enum, autoclass State(Enum):
ON = auto()
OFF = auto()

The auto function call the method _generate_next_value that returns the value of each member. It’s possible to override the method and define a custom one.

Note: It's not a good idea to mix constant assignments and the use of auto in the same enum.

Enums are more than only that

At the beginning of the article, we see how you can create an enum with the traditional way, but in fact, exists 4 main ways for creating an enum:

  1. The more simple way, creating a class that inherits from Enum.
from enum import Enum
class State(Enum):
ON = 1
OFF = 2

The disadvantage is that is not possible to make a direct comparison between the member value and another value.

Enum.ON < 100  # False, should be True
Enum.ON == 1 # False, should be True

Instead, you need to make an indirect comparison with the value attribute.

State.ON.value < 100  # True
State.ON.value == 1 # True

2. Creating a class that inherits from a mixin between enum.Enum and another type, one example of this is the IntEnum class.

The enum package includes the IntEnum class. In fact, it’s a shortcut for a class that inherits from int and Enum at the same time.

from enum import Enumclass State(int, Enum):
OFF = 0
ON = 1

Note: From Python 3.11 StrEnum is added, it’s a mix between str and Enum.

The principal advantage is that now the State members are instances of the inttype and is possible to compare directly with the integer value, without using the value attribute.

Enum.ON < 100  # True
Enum.ON == 1 # True

3. Creating a class that inheritance from enum.Flag

The previous enums have one downside, they can only referer to only one value per variable, but what happens if you require to referer to many members of the enum at the same time, for example, you need to referer to the days of the week that you have some class.

One simple implementation is with a normal Enum and an list variable.

from enum import Enumclass Weekday(Enum):
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 3
THURSDAY = 4
FRIDAY = 5
SATURDAY = 6
SUNDAY = 7
class_days = [Weekday.MONDAY, Weekday.TUESDAY, Weekday.THURSDAY]

Note: In this case, I prefer to assign the member's values (1, 2, 3, …) because they have a meaning in the context of the problem.

But Python gives us another Enum type that allows us to group many enum values in the same variables, this type of enum is named Flag:

from enum import Flagclass Weekday(Flag):
MONDAY = 1
TUESDAY = 2
WEDNESDAY = 4
THURSDAY = 8
FRIDAY = 16
SATURDAY = 32
SUNDAY = 64
class_days = Weekday.MONDAY | Weekday.TUESDAY | Weekday.THURSDAY

In this case, the members can be combined using the bitwise operators (&, |, ^, ~) and the result is still a Flag member.

class_days  # <Weekday.THURSDAY|TUESDAY|MONDAY: 11>

An important note is that the Flag implementation requires that the member’s values are integer values that follow the series of 2^n. If you prefer, you case use the auto function for skipping this step.

4. Creating an enum with the functional API

You can define an enum from a string:

Color = Enum('Color', 'RED YELLOW BLUE')

If you prefer, you can assign the value of the members with a sequence of tuples:

Color = Enum('Color', [('RED', 1), ('YELLOW', 2), ('BLUE', 3)])

Or a dictionary:

Color = Enum('Color', {'RED': 1, 'YELLOW': 2, 'BLUE': 3})

Finally, the enum is more than just constants, you can add methods like a normal class:

from enum import Enumclass Color(Enum):
RED = 0
BLUE = 1
YELLOW = 2
@classmethod
def favorite(cls):
return Color.BLUE

def opposite(self):
if self is Color.RED:
return Color.BLUE
...
...

A simple alternative

A simple alternative is to use strings instead of Enum, the problem is that is not always obvious what strings are supported, you need to read the documentation to know what is the set of strings that supports but with the introduction of type hints to python, this strategy can be improved.

Now, you can make use of unions of strings to specify that one string variable can have a limited set of values, for example:

import typing import Union, Literal
def change_state(state: Union[Literal['on'], Literal['off']]): ...

In this case, you make an explicit definition of the possible string that can take the place of the state parameter (‘on’ or ‘off’)

Note: If you are using Python 3.9+, you can make a union with Literal['on'] | Literal['off'], rather than using typing.Union.

Conclusions

  • Enum allows us to group a set of related constants.
  • Although the similarities with a definition class are great, the enum gives more powers and functionality to the member instances.
  • Exists many different ways of defining a class, and each one has its own advantages.
  • You can use the auto function to generate an automatic member value.
  • The enums, by default, can have an alias member (member with the same value as another member).
  • You can use unique as a decorator to generate an enum constrained for only unique members (not allowed to exist alias members).
  • The flag allows you to group many Enum instances with the bitwise operators.

Thanks for reading!

This is all for this post, I hope the content has been to your liking, see you in the next post.

More content at plainenglish.io. Sign up for our free weekly newsletter. Get exclusive access to writing opportunities and advice in our community Discord.

--

--