Enums

Intermediate45 min

Model a fixed set of choices with Enum, IntEnum and StrEnum, look members up by name and value, and branch on them with match.

What you will learn
  • Define an Enum and read the name and value of its members
  • Look up a member by value and by name, and iterate over all members
  • Choose between Enum, IntEnum and StrEnum, and explain how they compare to plain values
  • Use auto() for values, add methods to an enum, and match on its members
On this page

Programs are full of values that can only be one of a few things: a traffic light colour, an order status, a log level. Plain strings and numbers work until someone misspells one:

c23_strings_problem.py
def paint(color):
if color == "red":
return "stop"
return "unknown colour"
print(paint("red"))
print(paint("Red")) # same idea, different spelling
Output
stop
unknown colour

Python does not know that "Red" was meant to be "red", so the function quietly returned the wrong answer. An enumeration gives each choice a name and puts the full list in one place. A misspelled member name fails immediately instead of travelling through the program.

Defining an enum

Subclass Enum, and list the members as class attributes:

c23_basic.py
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
print(Color.RED)
print(repr(Color.RED))
print(Color.RED.name, Color.RED.value)
Output
Color.RED
<Color.RED: 1>
RED 1

Color.RED is a member: a single, unique object with two attributes, name (the text 'RED') and value (the 1 you assigned). The repr shows both. Member names are written in UPPER_CASE, like constants.

Looking members up

Call the enum with a value to find the member that has it. Index it with a name to find the member by name. Iterate over it to visit every member in definition order:

c23_lookup.py
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
print(Color(2))
print(Color["BLUE"])
for color in Color:
print(color.name, color.value)
print(Color.RED is Color.RED, Color.RED == Color.GREEN)
Output
Color.GREEN
Color.BLUE
RED 1
GREEN 2
BLUE 3
True False

Each member exists exactly once, so is works, and so does ==. Use whichever reads better. Comparing with is makes the “single object” idea explicit.

An invalid lookup raises an error of its own. By value it is a ValueError, and by name it is a KeyError:

c23_invalid.py
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
try:
Color(5)
except ValueError as error:
print("ValueError:", error)
try:
Color["PURPLE"]
except KeyError as error:
print("KeyError:", error)
Output
ValueError: 5 is not a valid Color
KeyError: 'PURPLE'

Letting Python choose the values: auto()

Often the values do not matter, and you only need the members to be distinct. auto() numbers them from 1:

c23_auto.py
from enum import Enum, auto
class Direction(Enum):
NORTH = auto()
EAST = auto()
SOUTH = auto()
WEST = auto()
print([d.value for d in Direction])
Output
[1, 2, 3, 4]

Members are not their values

A plain Enum member is a separate kind of object from the number it carries. It is deliberately not equal to it:

c23_not_int.py
from enum import Enum
class Color(Enum):
RED = 1
print(Color.RED == 1)
Output
False

That strictness protects you: a Color can never be mistaken for a quantity. It also means that when you read input from a file or from a user, you must convert it with Color(value). The conversion doubles as validation, because an unknown value raises ValueError.

IntEnum: members that are also integers

When the members must behave like integers, for example because they are compared with numbers or stored in a protocol, use IntEnum:

c23_intenum.py
from enum import IntEnum
class Priority(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
print(Priority.HIGH == 3)
print(Priority.HIGH > Priority.LOW)
print(sorted([Priority.HIGH, Priority.LOW, Priority.MEDIUM]))
print(Priority.MEDIUM + 1)
Output
True
True
[<Priority.LOW: 1>, <Priority.MEDIUM: 2>, <Priority.HIGH: 3>]
3

An IntEnum member equals its integer, sorts as an integer, and takes part in arithmetic. That convenience has a cost. The member can now be compared with any unrelated integer, which loses the protection of the plain Enum. Reach for IntEnum only when you need the integer behaviour.

StrEnum

StrEnum is the same idea for strings. Requires version 3.11 or later Its members are strings, so they compare with text, work in f-strings, and have all the string methods. With auto(), the value is the lower-case member name:

c23_strenum.py
from enum import StrEnum, auto
class Status(StrEnum):
PENDING = auto()
DONE = auto()
print(Status.PENDING == "pending")
print(Status("done"))
print(f"status is {Status.DONE}")
print(Status.DONE.upper())
Output
True
done
status is done
DONE

StrEnum is a good fit for statuses and options that travel as text, such as in JSON, a database column or a command-line flag. Python versions before 3.11 do not have it. There you would write class Status(str, Enum), which behaves similarly but formats differently in f-strings.

Adding behaviour

An enum is a class, so it can have methods, properties and class methods. The members are the instances:

c23_methods.py
from enum import Enum
class Weekday(Enum):
MON = 1
TUE = 2
SAT = 6
SUN = 7
@property
def is_weekend(self):
return self in (Weekday.SAT, Weekday.SUN)
@classmethod
def from_label(cls, label):
return cls[label.upper()]
print(Weekday.SAT.is_weekend, Weekday.MON.is_weekend)
print(Weekday.from_label("tue"))
Output
True False
Weekday.TUE

Inside the methods, self is a member, which is why self in (Weekday.SAT, Weekday.SUN) works. The class method from_label uses cls[...], the by-name lookup from above.

Branching with match

Requires version 3.10 or later Enum members work well with match. Write them as dotted names, Light.RED, so Python compares against the member:

c23_match.py
from enum import Enum
class Light(Enum):
RED = "red"
YELLOW = "yellow"
GREEN = "green"
def action(light):
match light:
case Light.RED:
return "stop"
case Light.YELLOW:
return "slow down"
case Light.GREEN:
return "go"
for light in Light:
print(light.name, "->", action(light))
Output
RED -> stop
YELLOW -> slow down
GREEN -> go

If you later add a fourth member and forget to handle it, a type checker can warn you that the match is not exhaustive. That check is one of the main reasons to use an enum over strings.

Common pitfalls

A bare name in a case. case RED: is a capture pattern. It matches anything and binds the name RED, rather than testing against a member. Always write the dotted form Light.RED.

Duplicate values make aliases. Two names with the same value do not create two members. The second is another name for the first:

c23_alias.py
from enum import Enum
class Status(Enum):
OK = 1
SUCCESS = 1 # same value: an alias, not a new member
FAILED = 2
print(Status.SUCCESS is Status.OK)
print(list(Status))
Output
True
[<Status.OK: 1>, <Status.FAILED: 2>]

SUCCESS is OK, and the list of members contains only two entries. If you want duplicates to be an error, decorate the enum with enum.unique.

Reassigning a member. Members are constants. Color.RED = 5 is an error:

c23_reassign.py
from enum import Enum
class Color(Enum):
RED = 1
try:
Color.RED = 5
except AttributeError:
print("AttributeError: members cannot be reassigned")
Output
AttributeError: members cannot be reassigned

Comparing a plain Enum with its value. Color.RED == 1 is False, as shown earlier. Compare members with members, or convert the incoming value first.

Using IntEnum by habit. Its members are equal to bare integers and to members of every other IntEnum. For a closed set of choices that should not mix with numbers, a plain Enum is safer.

ExerciseA traffic light state machine

Write an enum Light with members RED, GREEN and YELLOW, with the lower-case names as values. Give it a method next() that returns the next light in the cycle red, green, yellow, red. Give it a property can_go that is true only for green.

Start with Light.RED and run four steps. On each, print the step number, the member name and go or wait, then move to the next light. Expected output:

0 RED wait
1 GREEN go
2 YELLOW wait
3 RED wait
Show solution
c23_ex_traffic.py
from enum import Enum
class Light(Enum):
RED = "red"
GREEN = "green"
YELLOW = "yellow"
def next(self):
order = {
Light.RED: Light.GREEN,
Light.GREEN: Light.YELLOW,
Light.YELLOW: Light.RED,
}
return order[self]
@property
def can_go(self):
return self is Light.GREEN
light = Light.RED
for step in range(4):
print(step, light.name, "go" if light.can_go else "wait")
light = light.next()
Output
0 RED wait
1 GREEN go
2 YELLOW wait
3 RED wait

The dictionary inside next maps each member to its successor, which keeps the cycle in one readable place. The lesson used match for a similar job, and either works. can_go uses is to compare with the single GREEN member.

ExerciseParsing log levels

Requires version 3.11 or later Write a StrEnum called Level with members DEBUG, INFO and ERROR, whose values are the lower-case words. Then write parse_level(text). It strips spaces, lowers the case, and returns the matching member. For unknown text it raises ValueError with the message unknown level: 'loud' (with the real text, using !r), and it must hide the original enum error with from None.

Run it on "INFO", " error " and "loud". For each good one print the member name and value, for the bad one print error: and the message. Expected output:

INFO info
ERROR error
error: unknown level: 'loud'
Show solution
c23_ex_parse.py
from enum import StrEnum
class Level(StrEnum):
DEBUG = "debug"
INFO = "info"
ERROR = "error"
def parse_level(text):
try:
return Level(text.strip().lower())
except ValueError:
raise ValueError(f"unknown level: {text!r}") from None
for text in ["INFO", " error ", "loud"]:
try:
level = parse_level(text)
print(level.name, level.value)
except ValueError as error:
print("error:", error)
Output
INFO info
ERROR error
error: unknown level: 'loud'

Level(...) raises ValueError for an unknown value. The function converts it to a message in the program’s own words, and from None removes the confusing “during handling of the above exception” chain, as the earlier lesson on chaining explained.

ExercisePredict the comparisons

Plain is an ordinary Enum and Number is an IntEnum, and both have a member with the value 1. Write down the four printed lines before you run this, and explain the third one.

c23_ex_predict.py
from enum import Enum, IntEnum
class Plain(Enum):
ONE = 1
class Number(IntEnum):
ONE = 1
print(Plain.ONE == 1)
print(Number.ONE == 1)
print(Number.ONE == Plain.ONE)
print(Number.ONE + 1)
Show solution
False
True
False
2

A plain Enum member is not equal to its value, so the first line is False. An IntEnum member is an integer, so the second is True and the arithmetic in the fourth line gives the plain integer 2. The third compares an IntEnum member with a plain Enum member. The Plain member is not an integer and does not know how to equal Number.ONE, so the answer is False.

Summary

  • An Enum is a fixed set of named members. Each has a name and a value, and each exists exactly once.
  • Color(1) looks up by value (ValueError if missing), Color["RED"] looks up by name (KeyError if missing), and iterating yields every member.
  • Plain enum members do not equal their values. IntEnum members are integers. StrEnum Requires version 3.11 or later members are strings.
  • auto() picks distinct values for you. Enums can have methods, properties and class methods.
  • In match, write members as dotted names. Duplicate values create aliases, not new members.