Classes and objectsChapter 68 of 114
Enums
A fixed set of named values, instead of loose strings scattered about.
The problem with loose strings
def move(direction):
if direction == "north":
return "going up"
return "unknown"
print(move("north"))
print(move("North"))Output
going up unknown
A typo or a stray capital gives a wrong answer with no error. Nothing lists the valid values, so nothing can check them.
An enum names them
from enum import Enum
class Direction(Enum):
NORTH = "north"
SOUTH = "south"
print(Direction.NORTH)
print(Direction.NORTH.name)
print(Direction.NORTH.value)Output
Direction.NORTH NORTH north
Direction.NORHT is an AttributeError at the moment you write it, rather than a wrong answer much later.
Members are singletons
Compare with is. Each member exists exactly once:
from enum import Enum
class Direction(Enum):
NORTH = "north"
SOUTH = "south"
a = Direction.NORTH
b = Direction("north")
print(a is b)
print(a == Direction.SOUTH)
print(Direction["NORTH"] is a)Output
True False True
Look one up by value with Direction(value), and by name with Direction["NAME"]. An unknown value raises:
from enum import Enum
class Direction(Enum):
NORTH = "north"
try:
Direction("up")
except ValueError as problem:
print("ValueError:", problem)Output
ValueError: 'up' is not a valid Direction
That is validation you did not have to write.
Iterating and membership
from enum import Enum
class Direction(Enum):
NORTH = "north"
SOUTH = "south"
EAST = "east"
print([d.name for d in Direction])
print(len(Direction))
print(Direction.NORTH in Direction)Output
['NORTH', 'SOUTH', 'EAST'] 3 True
The list of valid values is now part of the type, so a form dropdown or a validation message can be generated from it.
auto, when the value does not matter
from enum import Enum, auto
class Status(Enum):
PENDING = auto()
ACTIVE = auto()
DONE = auto()
print(Status.ACTIVE.value)
print([s.name for s in Status])Output
2 ['PENDING', 'ACTIVE', 'DONE']
Use auto() when nothing outside your program sees the value. Give explicit values when they are stored in a database or sent over an API, so a reordering cannot change their meaning.
StrEnum and IntEnum
A plain Enum member is not equal to its value. When it has to interoperate with plain strings, use StrEnum:
from enum import Enum, StrEnum
class Loose(Enum):
NORTH = "north"
class Tight(StrEnum):
NORTH = "north"
print(Loose.NORTH == "north")
print(Tight.NORTH == "north")
print(Tight.NORTH.upper())Output
False True NORTH
StrEnum members are strings, so they slot into existing code that expects one. IntEnum does the same for numbers.
With match
Pattern matching and enums fit together neatly:
from enum import Enum
class Status(Enum):
PENDING = "pending"
DONE = "done"
def describe(status):
match status:
case Status.PENDING:
return "not started"
case Status.DONE:
return "finished"
print(describe(Status.PENDING))
print(describe(Status.DONE))Output
not started finished
Note this is a dotted name, so it compares rather than binding — the one case where match does what you would assume.
Test yourself
2 questionsWhat is the advantage of an enum over loose strings?
Show the answer
A typo is an AttributeError where you wrote it, not a wrong answer later — You also get the list of valid values as part of the type, which nothing can drift out of sync with.
How does StrEnum differ from Enum?
Show the answer
Its members are strings, so they equal their value — That makes it drop into existing code that expects a plain string. A plain Enum member is not equal to its value.
Magic Methods
The dunder methods that make your class work with Python's own syntax.