Python Enum: Named Constants with the enum Module
Stop Using Magic Strings and Numbers
Many programs have variables that can only take a few fixed values: an order is "pending", "paid" or "shipped"; a traffic light is red, amber or green; a grade is A to F. Storing these as plain strings or numbers invites bugs: a typo like "payed" is accepted silently, and nobody remembers what status 3 means. A Python enum (enumeration) defines a fixed set of named constants, so typos become errors, code becomes self-explaining, and editors can auto-complete the options. This tutorial covers creating enums with the enum module, members' names and values, iteration and lookup, auto(), comparing members, adding methods, IntEnum and StrEnum, Flag for combinable options, and using enums with the match statement. All outputs shown are from running the code.
Creating a Python Enum
Subclass Enum and list the members as class attributes:
from enum import Enum
class OrderStatus(Enum):
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
CANCELLED = "cancelled"
status = OrderStatus.PAID
print(status)
print(status.name, status.value)
print(type(status).__name__, isinstance(status, OrderStatus))
Output:
OrderStatus.PAID PAID paid OrderStatus True
Each member has a name (the identifier) and a value (what it was set to). By convention, member names are in capitals because they are constants.
Why Enums Catch Bugs
With plain strings, a typo is just another string. With an enum, it is an error the moment the code runs:
try:
s = OrderStatus.PAYED
except AttributeError as e:
print("AttributeError:", e)
try:
s = OrderStatus("refunded")
except ValueError as e:
print("ValueError:", e)
Output:
AttributeError: type object 'OrderStatus' has no attribute 'PAYED' ValueError: 'refunded' is not a valid OrderStatus
Members also cannot be reassigned: OrderStatus.PAID = "x" raises an error, so the set of options can't be changed by accident.
Looking Up and Looping Over Members
You can get a member from its value with a call, or from its name with square brackets. This is how you convert data coming from a form, a database or JSON back into an enum:
from_db = "shipped"
print(OrderStatus(from_db)) # by value
print(OrderStatus["CANCELLED"]) # by name
for s in OrderStatus:
print(s.name.title(), "->", s.value)
print(len(OrderStatus), [s.value for s in OrderStatus][:2])
Output:
OrderStatus.SHIPPED OrderStatus.CANCELLED Pending -> pending Paid -> paid Shipped -> shipped Cancelled -> cancelled 4 ['pending', 'paid']
Members are listed in the order they were defined, which makes enums good for building drop-down menus.
Comparing Members
Compare enum members with is or ==. Each member is a single object (a singleton), so is is safe and common. A plain Enum member is not equal to its raw value:
status = OrderStatus.PAID print(status is OrderStatus.PAID, status == OrderStatus.PAID) print(status == "paid", status.value == "paid")
Output:
True True False True
This strictness is deliberate: it stops you mixing up enum members with arbitrary strings. Compare .value when you really mean the raw value, or use StrEnum (below).
auto() and Unique Values
When the actual values don't matter, auto() numbers the members for you. The @unique decorator raises an error if two members share a value (otherwise the second becomes an alias of the first):
from enum import auto, unique
@unique
class Level(Enum):
BEGINNER = auto()
INTERMEDIATE = auto()
ADVANCED = auto()
print([(m.name, m.value) for m in Level])
Output:
[('BEGINNER', 1), ('INTERMEDIATE', 2), ('ADVANCED', 3)]
Adding Methods and Properties
Enums are classes, so they can carry behaviour. Here each grade knows its points on the Nigerian 5-point scale and whether it is a pass:
class Grade(Enum):
A = 5
B = 4
C = 3
D = 2
E = 1
F = 0
@property
def points(self):
return self.value
def is_pass(self):
return self is not Grade.F
@classmethod
def from_score(cls, score):
for limit, grade in [(70, cls.A), (60, cls.B), (50, cls.C), (45, cls.D), (40, cls.E)]:
if score >= limit:
return grade
return cls.F
for score in [82, 55, 38]:
g = Grade.from_score(score)
print(score, g.name, g.points, g.is_pass())
Output:
82 A 5 True 55 C 3 True 38 F 0 False
Putting the logic inside the enum keeps all grade rules in one place instead of scattering if statements across the program.
IntEnum and StrEnum
Sometimes you need members that behave like ordinary numbers or strings, for example when sending them to an API or comparing with existing data. IntEnum members are also integers, and StrEnum (Python 3.11+) members are also strings:
from enum import IntEnum, StrEnum
class Priority(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
class Colour(StrEnum):
RED = "red"
GREEN = "green"
print(Priority.HIGH > Priority.LOW, Priority.MEDIUM + 1)
print([p.name for p in sorted([Priority.HIGH, Priority.LOW, Priority.MEDIUM])])
print(Colour.RED == "red", Colour.GREEN.upper(), f"{Colour.RED}")
Output:
True 3 ['LOW', 'MEDIUM', 'HIGH'] True GREEN red
Use these only when you need the mixing; plain Enum gives stronger protection against accidental comparisons.
Flag: Combining Options
A Flag enum lets members be combined with |, which is ideal for permissions or settings where several can be on at once:
from enum import Flag
class Permission(Flag):
READ = auto()
WRITE = auto()
DELETE = auto()
editor = Permission.READ | Permission.WRITE
print(Permission.WRITE in editor, Permission.DELETE in editor)
print([p.name for p in Permission if p in editor])
print(Permission.READ.value, Permission.WRITE.value, Permission.DELETE.value)
Output:
True False ['READ', 'WRITE'] 1 2 4
auto() gives a Flag powers of two (1, 2, 4, …), so each option is a separate bit, exactly like file permissions in Linux.
Enums with the match Statement
Python's match statement (3.10+) works neatly with enum members:
def next_step(status):
match status:
case OrderStatus.PENDING:
return "Wait for payment"
case OrderStatus.PAID:
return "Pack and ship"
case OrderStatus.SHIPPED:
return "Track delivery"
case _:
return "No action"
for s in OrderStatus:
print(s.value, "=>", next_step(s))
Output:
pending => Wait for payment paid => Pack and ship shipped => Track delivery cancelled => No action
Dotted names like OrderStatus.PAID are treated as values to compare against, not as new variables to capture, which is exactly what we want here.
Storing Enums in JSON or a Database
JSON and databases don't understand enum objects, so store member.value (or member.name) and convert back with OrderStatus(value) when loading. Storing the value keeps the data readable and survives renaming a member in code, as long as the value stays the same.
Summary
- A Python enum defines a fixed set of named constants; each member has a
nameand avalue. - Typos and invalid values raise errors immediately instead of silently creating bad data.
- Look up members with
Enum(value)orEnum["NAME"]; loop over them in definition order. - Compare members with
is; plain enum members are not equal to raw strings or numbers. auto(),@unique, methods,IntEnum,StrEnumandFlagcover most real needs.