Command Palette

Search for a command to run...

Hectal
PHASE 8Intermediate ~28 min· topic 9 of 12

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 with in.
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.

main.pywhole filepython
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.

main.pywhole filepython
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. 1

    Predict the comparisons

    Before running the first example, decide what Color.RED == 1 prints. It is False. Now change Color(Enum) to class Color(IntEnum) and run again: the answer flips to True.

  2. 2

    Add a member

    Add PAUSED = "paused" to a copy of FinishReason and loop over the enum again. Notice next_action returns None for the new member, since no case handles it. That quiet gap is why many teams add a final case _: that raises an error.

  3. 3

    Combine flags

    Create everything = Perm.READ | Perm.WRITE | Perm.RUN and print list(Perm) and everything. Then remove one permission with everything & ~Perm.WRITE. (~ means "everything except this one", and & keeps only what both sides share.)

Code & diagrams

Enum, auto, IntEnum and Flag New tab

Try Color(3), Color['GREEN'] and Color.RED < Color.BLUE (that last one raises TypeError, because a plain Enum has no ordering).

Sign in to run this example in your browser.

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.WRITE
StrEnum, match, methods and parsing at the edge Python 3.11+ New tab

StrEnum 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).

Sign in to run this example in your browser.

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>]
Before StrEnum (Python 3.10 and earlier, not runnable)python

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"
Raw text becomes a checked memberdiagram
Rendering diagram…

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.

main.pywhole filepython
from enum import Enum


class Color(Enum):
    RED = 1


Color.RED = 5
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 8, in <module>
Color.RED = 5
...
AttributeError: cannot reassign member 'RED'

Break #2

Misspell a member

Write Color.PINK when there is no such member.

main.pywhole filepython
from enum import Enum


class Color(Enum):
    RED = 1


print(Color.PINK)
terminal
$ python main.py
── what you'll see ──
Traceback (most recent call last):
File "main.py", line 8, in <module>
print(Color.PINK)
^^^^^^^^^^
AttributeError: type object 'Color' has no attribute '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 Enum or StrEnum over IntEnum unless you truly need number behaviour. IntEnum members 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.dumps writes StrEnum and IntEnum members as their plain value, but raises TypeError: Object of type ... is not JSON serializable for a plain Enum, so pass .value yourself. StrEnum with auto() uses the lower-cased member name as the value (TOOL_CALLS becomes "tool_calls").

  • ▸

    Override the classmethod _missing_(cls, value) to make lookups forgiving, for example case-insensitive (Role("USER") finding Role.USER) or mapping legacy values; return None to let the normal ValueError happen. Since Python 3.12, x in MyEnum also accepts raw values ("stop" in FinishReason); earlier versions raise TypeError for 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, prefer IntFlag only when interacting with C APIs or file permission bits; otherwise Flag avoids accidental integer mixing.

Remember this

  1. 1

    Without enums, programs use magic values: status = 2 (what does 2 mean?) or status = "actve" (a typo nobody notices). An enum gives each choice a name and a value. You write class Color(Enum): and list members like RED = 1. Then Color.RED is a unique, readable object, and a typo like Color.REDD fails immediately with an AttributeError.

  2. 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 raises ValueError. Iterate with for c in Color or list(Color), in definition order. Members are singletons (there is only ever one object for each member): Color(1) is Color.RED is True, so compare with is or ==.

  3. 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 plain Enum: Color.RED == 1 is False. That is a feature, because it stops accidental mixing with raw numbers. When you want number behaviour, use IntEnum, whose members are real ints (Level.HIGH == 10 and Level.MID + 1 work).

  4. 4

    StrEnum (Python 3.11) makes members real strings: FinishReason.LENGTH == "length" is True, and an f-string prints the value. It is ideal for values that travel as text in JSON or APIs. Before 3.11 people wrote class X(str, Enum). A plain Enum also works with text values; you convert at the boundary with Role("user").

  5. 5

    Flag represents combinations of options, like a panel of on/off switches. You combine members with |: Perm.READ | Perm.WRITE. Test with in: 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 for Flag. Use it for permissions or feature switches, not for ordinary exclusive choices.

  6. 6

    Enums can have methods, properties and extra data. A planet enum can store mass and radius per member through __init__ and offer a computed gravity property. Two members with the same value become aliases (the second name points to the first member), which the @unique decorator forbids.

  7. 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

01

What problem do enums solve, and how do you define one?

02

How do you look up an enum member by value and by name, and what if the value is invalid?

03

What is the difference between Enum, IntEnum and StrEnum?

04

How do enums work with match, and what is the capture-pattern trap?

Practice

01

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.

02

Write a StrEnum called Role with USER, ASSISTANT and SYSTEM (lowercase text values). Parse "assistant" into a member and print role == "assistant".

03

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.

  • ↔

    **IntEnum and StrEnum are convenient because they act like numbers or strings, but they weaken the type safety of a plain Enum.** 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 .name and .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, StrEnum and Flag.

  • Done when you can use an enum with match using dotted names.

  • Done when you can parse raw text into an enum at a program boundary.