Topic 8.9
Enums
In one line
An enum is a fixed set of named constants, such as Color.RED or FinishReason.STOP. Enums replace magic numbers and loose strings with names that are checked, listable and safe to compare, and they pair naturally with match.
Think of it like this
The buttons on a lift. The choices are fixed: 1, 2, 3 or Open. You cannot press "2.5" or "banana". An enum (short for enumeration) is a set of allowed choices with names. A variable that holds a lift button can only be one of those buttons, and that makes mistakes impossible to miss.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Enum
- A class whose members are a fixed set of named constants.
- Member
- One named constant of an enum, such as
Color.RED. - `.name` / `.value`
- The member's name as text, and the data attached to it.
- `auto()`
- Asks Python to pick the values for you, starting at 1.
- `IntEnum`
- An enum whose members are also integers, so they compare and add like numbers.
- `StrEnum`
- An enum whose members are also strings (Python 3.11).
- `Flag`
- An enum whose members can be combined with
|and tested within. - Alias
- A second name for a member that has the same value as an earlier one.
- Magic value
- A bare number or string in code whose meaning is not obvious or checked.
Step by step
01The problem with magic values
status = "actve" is a valid string, so Python happily accepts it and the bug appears much later, somewhere else. if status == 2 forces readers to remember what 2 means.
An enum turns the choices into names that exist in one place.
02Define and use an enum
Color.RED prints as Color.RED; its repr is <Color.RED: 1>. Members have .name and .value. The class itself is iterable, in the order you wrote the members.
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
print(Color.RED.name, Color.RED.value)
print(list(Color))03Look up by value and by name
Color(2) finds the member whose value is 2. Color["BLUE"] finds the member whose name is BLUE. A value that does not exist raises ValueError: 9 is not a valid Color. This is how you turn data coming from a file or an API into a safe member.
04Enum members are not their values
In a plain Enum, Color.RED == 1 is False. That stops bugs like comparing a colour to a price. Use IntEnum for members that need to act like numbers, such as log levels: they sort and compare with plain ints.
05StrEnum for text that travels
APIs send values like "stop" and "length" as text. A StrEnum member is the string, so reason == "length" is True and reason.upper() works. Build it from the incoming text with FinishReason("length"), and any unknown value fails right there, not later.
StrEnum needs Python 3.11 or later.
06Enums with match
A match statement can branch on members. case FinishReason.STOP: uses a dotted name, so Python compares to the member. (A bare name like case STOP: would capture the value into a new variable instead, which is a classic bug. If another case follows it, Python reports SyntaxError: name capture 'STOP' makes remaining patterns unreachable; if it is the last case, nothing warns you.) Branching over every member also makes it obvious when you forget one.
match reason:
case FinishReason.STOP:
return "show the answer"
case FinishReason.LENGTH:
return "ask the model to continue"07Flags, extra data and aliases
Perm.READ | Perm.WRITE builds a combination; Perm.READ in mine tests it. Members can carry extra data: the Planet enum stores mass and radius, and a property computes gravity from them.
Two names with the same value make an alias: Shape.BOX is Shape.SQUARE. Add @unique from enum if you want that to be an error.
Try it yourself
- 1
Predict the comparisons
Before running the first example, decide what
Color.RED == 1prints. It isFalse. Now changeColor(Enum)toclass Color(IntEnum)and run again: the answer flips toTrue. - 2
Add a member
Add
PAUSED = "paused"to a copy ofFinishReasonand loop over the enum again. Noticenext_actionreturnsNonefor the new member, since no case handles it. That quiet gap is why many teams add a finalcase _:that raises an error. - 3
Combine flags
Create
everything = Perm.READ | Perm.WRITE | Perm.RUNand printlist(Perm)andeverything. Then remove one permission witheverything & ~Perm.WRITE. (~means "everything except this one", and&keeps only what both sides share.)
Code & diagrams
Try Color(3), Color['GREEN'] and Color.RED < Color.BLUE (that last one raises TypeError, because a plain Enum has no ordering).
Expected output
Color.RED
<Color.RED: 1>
RED 1
Color.GREEN Color.BLUE
[<Color.RED: 1>, <Color.GREEN: 2>, <Color.BLUE: 3>]
['RED', 'GREEN', 'BLUE']
True False
False
True
1 RED
2 GREEN
3 BLUE
ValueError: 9 is not a valid Color
[('FIRST', 1), ('SECOND', 2), ('THIRD', 3)]
True True 6 [<Level.LOW: 1>, <Level.HIGH: 10>]
Perm.READ|WRITE
True False
Perm(0) Perm.WRITEStrEnum needs Python 3.11 and match needs Python 3.10. Add a new finish reason and notice that next_action returns None until you add a case. In Role.parse, raise ... from None hides the earlier error so the learner sees only the clear message (Topic 7.2).
Expected output
length True LENGTH
finished because: length
STOP -> show the answer
LENGTH -> ask the model to continue
TOOL_CALLS -> run the requested tools
9.82 3.7
Msg(role=<Role.USER: 'user'>, text='hi')
ValueError: unknown role: robot
True [<Shape.SQUARE: 1>]On Python 3.11 and later prefer StrEnum. One difference: format() and str() of a (str, Enum) member changed across versions, which is one reason StrEnum was added.
from enum import Enum
class FinishReason(str, Enum): # the older spelling of StrEnum
STOP = "stop"
LENGTH = "length"Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Change a member
Assign a new value to an enum member.
from enum import Enum
class Color(Enum):
RED = 1
Color.RED = 5Break #2
Misspell a member
Write Color.PINK when there is no such member.
from enum import Enum
class Color(Enum):
RED = 1
print(Color.PINK)Myth vs fact
Myth
Enums are the same as constants or just a dictionary.
Fact
Members are unique objects with a type, a name, a value and iteration. A typo is an error, and isinstance(x, Color) tells you something real, which a dict key or bare number cannot.
Myth
Color.RED == 1 is True if RED = 1.
Fact
Not for a plain Enum. Use Color.RED.value == 1, or use IntEnum when numeric behaviour is wanted.
Myth
case RED: in a match compares against Color.RED.
Fact
A bare name is a capture pattern: it matches anything and assigns it to RED. Python raises a SyntaxError if more cases follow it, but a bare name in the last case matches everything without any warning. Use the dotted name case Color.RED: to compare with the member.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Prefer plain
EnumorStrEnumoverIntEnumunless you truly need number behaviour.IntEnummembers equal plain integers, so they can leak into arithmetic and into other enums' comparisons, which defeats the type safety. - ▸
Enums are the natural bridge between loose JSON strings and typed code. Pydantic (Phase 16) accepts an enum-typed field and rejects unknown values with a clear message, and a JSON Schema for tool definitions lists the allowed values from the enum.
- ▸
JSON behaviour matters at API boundaries:
json.dumpswritesStrEnumandIntEnummembers as their plain value, but raisesTypeError: Object of type ... is not JSON serializablefor a plainEnum, so pass.valueyourself.StrEnumwithauto()uses the lower-cased member name as the value (TOOL_CALLSbecomes"tool_calls"). - ▸
Override the classmethod
_missing_(cls, value)to make lookups forgiving, for example case-insensitive (Role("USER")findingRole.USER) or mapping legacy values; returnNoneto let the normalValueErrorhappen. Since Python 3.12,x in MyEnumalso accepts raw values ("stop" in FinishReason); earlier versions raiseTypeErrorfor non-members. - ▸
Enum members are created once at class-definition time and are hashable, so they make good dictionary keys and are safe to share between threads and processes (pickling stores the class path and the name).
- ▸
For
Flag, preferIntFlagonly when interacting with C APIs or file permission bits; otherwiseFlagavoids accidental integer mixing.
Remember this
- 1
Without enums, programs use magic values:
status = 2(what does 2 mean?) orstatus = "actve"(a typo nobody notices). An enum gives each choice a name and a value. You writeclass Color(Enum):and list members likeRED = 1. ThenColor.REDis a unique, readable object, and a typo likeColor.REDDfails immediately with anAttributeError. - 2
Every member has a
.name(the text"RED") and a.value(the data,1). You can look a member up by value,Color(2), and by name,Color["BLUE"]. A value that does not exist raisesValueError. Iterate withfor c in Colororlist(Color), in definition order. Members are singletons (there is only ever one object for each member):Color(1) is Color.REDisTrue, so compare withisor==. - 3
Use
auto()when you do not care about the values: Python assigns 1, 2, 3 and so on. Members are not equal to their values in a plainEnum:Color.RED == 1isFalse. That is a feature, because it stops accidental mixing with raw numbers. When you want number behaviour, useIntEnum, whose members are real ints (Level.HIGH == 10andLevel.MID + 1work). - 4
StrEnum(Python 3.11) makes members real strings:FinishReason.LENGTH == "length"isTrue, and an f-string prints the value. It is ideal for values that travel as text in JSON or APIs. Before 3.11 people wroteclass X(str, Enum). A plainEnumalso works with text values; you convert at the boundary withRole("user"). - 5
Flagrepresents combinations of options, like a panel of on/off switches. You combine members with|:Perm.READ | Perm.WRITE. Test within:Perm.READ in mine. Under the hood each member is one bit, so its number is a power of two (1, 2, 4, 8 and so on);auto()picks these forFlag. Use it for permissions or feature switches, not for ordinary exclusive choices. - 6
Enums can have methods, properties and extra data. A planet enum can store mass and radius per member through
__init__and offer a computedgravityproperty. Two members with the same value become aliases (the second name points to the first member), which the@uniquedecorator forbids. - 7
Enums match nicely with
match(Python 3.10, Topic 2.8):case FinishReason.STOP:compares against the member (the dotted name means "compare to this value", not "capture into a variable"). In AI work, enums describe roles (user,assistant), finish reasons (stop,length,tool_calls), and agent states. Parse the raw string into an enum at the edge of your program, and the rest of your code works with safe names.
Explain it without notes
What problem do enums solve, and how do you define one?
How do you look up an enum member by value and by name, and what if the value is invalid?
What is the difference between Enum, IntEnum and StrEnum?
How do enums work with match, and what is the capture-pattern trap?
Practice
Write an enum Weekday with MONDAY to SUNDAY using auto(), and a function is_weekend(day) that returns True for Saturday and Sunday. Test Weekday.SUNDAY and Weekday.MONDAY.
Write a StrEnum called Role with USER, ASSISTANT and SYSTEM (lowercase text values). Parse "assistant" into a member and print role == "assistant".
Write a function describe(level) using match on an IntEnum Level with LOW = 1, HIGH = 3 that returns "relax" or "hurry".
Trade-offs
- ↔
Enums make invalid values impossible to type by accident, at the cost of converting at the boundaries. You must turn incoming text into members; that single conversion is where bad data is caught.
- ↔
**
IntEnumandStrEnumare convenient because they act like numbers or strings, but they weaken the type safety of a plainEnum.** Use them for values that really travel as ints or text.
Done when you can
Done when you can define an enum, iterate it and read
.nameand.value.Done when you can look up members by value and by name and explain the errors.
Done when you can explain when to use
Enum,IntEnum,StrEnumandFlag.Done when you can use an enum with
matchusing dotted names.Done when you can parse raw text into an enum at a program boundary.