🔢 Enumerations in Python

Enumerations provide a developer with a restricted set of values.


Introduction

In some scenarios, you want a developer to pick one value from a list. Colors of a traffic light, HTTP methods and HTTP response status codes are all great examples of this type of relationship. To express that relationship in Python, you should use enumerations. Enumerations are a construct that let you define the list of values, and developers pick the specific value they want. Python first supported enumerations in Python 3.4.

Let's suppose I represent the traffic lights as a Python tuple:

# Node: use UPPER_CASE variable names to denote constant/immutable values
TRAFFIC_LIGHTS = ("Red", "Yellow", "Green")

What does this tuple communicate to other developers?

  • This collection is immutable.
  • They can iterate over this collection to get all the lights.
  • They can retrieve a specific light through static indexing.

The immutability and retrieval properties are important for my application. I don't want to add or subtract any lights at runtime. Retrieval lets me choose just one light, but it is a bit clunky.

TRAFFIC_LIGHTS[1]

This unfortunately does not communicate intent. Every time a developer sees this, they must remember that 1 means Yellow. Constantly correlating numbers to lights wastes time. This is fragile and will invariably cause mistakes. To combat this, I'll make aliases for each of these:

RED = "Red"
YELLOW = "Yellow"
GREEN = "Green"
TRAFFIC_LIGHTS = (RED, YELLOW, GREEN)

That's a bit more code, and still doesn't make it any easier to index into that tuple. Futhermore, there is still a lingering issue in calling code. Consider a function that performs an action based on the light:

def act(light: str):
    ...

Future developers would come across code like this:

act(TRAFFIC_LIGHTS[0])
act(RED)

Or:

act("Red")
# Definitely wrong
act("Red Yellow Green")

And here lies the crux of the problem. On the happy path, a developer can use the predefined variables. But if somebody accidentally were to use the wrong light, you soon get unwanted behavior. You want to find a way to communicate that you want a very specific, restricted set of values in specific locations.

Enum

Here's an example of Python's enumeration, Enum, in action:

from enum import Enum
# Class syntax
class TrafficLight(Enum):
    RED = "Red"
    YELLOW = "Yellow"
    GREEN = "Green"
# Functional syntax
TrafficLight = Enum('TrafficLight', ['RED', 'YELLOW', 'GREEN'])

To access specific instances, you can just do:

TrafficLight.RED
TrafficLight.GREEN

If you wanted to print out all the values of the enumeration, you can simply iterate over the enumeration:

for option_number, light in enumerate(TrafficLight, start=1):
    print(f"Option {option_number}: {light.value}")
# Option 1: Red
# Option 2: Yellow
# Option 3: Green

Finally, you can communicate your intent in functions that use this Enum:

def act(light: TrafficLight):
    ...

This tells all the developers looking ath this function that they should be passing in a TrafficLight enumeration, and not just any old string. It becomes much harder to introduce typos or incorrect values.

When Not to Use

Enumerations are great for communicating a static set of choices for users. You don't want to use them where your options are determined at runtime, as you lose a lot of their benefits around communicating intent and tooling. If you find yourself in this situation, a dictionary which offers a natural mapping between two values that can be changed at runtime would be a better choice. You will need to perform membership checks if you need to restrict what values a user an select, though.

Advanced Usage

Automatic Values

For some enumeration, you might want to explicitly specify that you don't care about the value that the enumeration is tied on. This tells users that they should not rely on these values. For this, you can use the auto function.

from enum import auto, Enum
class TrafficLight(Enum):
    RED = auto()
    YELLOW = auto()
    GREEN = auto()
print(list(TrafficLight))
# [<TrafficLight.RED: 1>, <TrafficLight.YELLOW: 2>, <TrafficLight.GREEN: 3>]

By default, auto will select monotonically increasing values(1, 2, 3...). If you would like to control what values are set, you should implement a _generate_next_value_ function:

from enum import auto, Enum
class TrafficLight(Enum):
    def _generate_next_value_(name, start, count, last_values):
        return name.capitalize()
    RED = auto()
    YELLOW = auto()
    GREEN = auto()
print(list(TrafficLight))
# [<TrafficLight.RED: 'Red'>, <TrafficLight.YELLOW: 'Yellow'>, <TrafficLight.GREEN: 'Green'>]

Flags

Now that you have the traffic lights represented in an Enum, we can declare a variable stop_signs for when the light is Red or Yellow, you may track a list of lights as such:

from typing import Set
stop_signs: Set[TrafficLight] = {TrafficLight.RED, TrafficLight.YELLOW}

This tells readers that a collection of traffic lights will be unique, and that there might be zero, one, or many lights. This satisfies your needs. But I don't want to rely on every developer remembering to use a set(just one use of a list or dictionary can invite wrong behavior). I want some way to represent a grouping of unique enumeration values and more intuitive. The enum module gives you a handy base class to use - Flag:

from enum import auto, Flag
class TrafficLight(Flag):
    RED = auto()
    YELLOW = auto()
    GREEN = auto()

This lets you perform bitwise operations to combine traffic lights or check if certain lights are present.

stop_signs = TrafficLight.RED | TrafficLight.YELLOW
if stop_signs & TrafficLight.RED:
    print("You should wait!")
if stop_signs ^ TrafficLight.GREEN:
    print("You can go!")

Unique

One great feature of enumerations is the ability to alias values. However, there are cases where you want to force uniqueness on the values. Perhaps you are relying on the enumeration to always contain a set number of values, or perhaps it messes with some of the string representations that are shown to customers. No matter the case, if you want to preserve uniqueness in your Enum, simply add a @unique decorator.

from enum import Enum, unique
@unique
class TrafficLight(Enum):
    RED = "Red"
    YELLOW = "Yellow"
    GREEN = "Green"

Integer Conversion

There are two more special case enumerations called IntEnum and IntFlag. These map to Enum and Flag, respectively, but allow degradation to raw integers for comparison. But I do not recommend using these features, because the situation to use them is when you want compare enumerations and integers directly, this may cause error if the value of an Enum changes in the future.

Summary

Enumerations are simple, and often overlooked as a powerful communication method. Any time that you want to represent a single value from a static collection of values, an enumeration should be your go-to user-defined type. It's easy to define and use them. They offer a wealth of operations, including iteration, bitwise operations and control over uniqueness.