Before I discovered Python’s enum module, I represented fixed sets of options using plain strings or integer constants scattered across my code — status = "pending", status = "active", and so on. It worked until typos crept in ("pendign" anyone?) and I had no way to enforce that a variable could only hold one of a specific set of values. Switching to Enum fixed that entirely, and it’s now one of the first things I reach for when modeling a fixed set of related constants. This guide covers everything from the basics of defining an enum to how it works internally and where it genuinely improves real code.
What Is an Enum
An enumeration (Enum) is a set of symbolic names bound to unique, constant values. Instead of scattering string or integer literals throughout code, an enum groups related constants under a single, well-defined type.
from enum import Enum
class Status(Enum):
PENDING = 1
ACTIVE = 2
COMPLETED = 3
CANCELLED = 4
print(Status.ACTIVE) # Status.ACTIVE
print(Status.ACTIVE.name) # 'ACTIVE'
print(Status.ACTIVE.value) # 2
Why I Use Enums Instead of Plain Constants
Before enums, this was common in my code:
STATUS_PENDING = "pending"
STATUS_ACTIVE = "active"
def update_status(order, status):
if status not in ("pending", "active", "completed"):
raise ValueError("Invalid status")
order.status = status
With enums, invalid values become structurally impossible rather than something I have to manually validate every time:
from enum import Enum
class Status(Enum):
PENDING = "pending"
ACTIVE = "active"
COMPLETED = "completed"
def update_status(order, status: Status):
order.status = status # status is guaranteed to be a valid Status member
Type checkers like mypy can also catch mismatches at development time, something plain strings never allowed.
Accessing Enum Members
There are multiple ways to access enum members, each useful in different contexts:
class Status(Enum):
PENDING = 1
ACTIVE = 2
print(Status.PENDING) # By attribute access
print(Status["PENDING"]) # By name (string) lookup
print(Status(1)) # By value lookup
for status in Status:
print(status.name, status.value)
Value lookup (Status(1)) is especially useful when deserializing data from a database or API where I only have the raw value, not the name.
raw_value_from_db = 2
status = Status(raw_value_from_db)
print(status) # Status.ACTIVE
Comparing Enum Members
Enum members compare by identity, not just equality, and are automatically singleton instances:
print(Status.ACTIVE == Status.ACTIVE) # True
print(Status.ACTIVE is Status.ACTIVE) # True
print(Status.ACTIVE == Status.PENDING) # False
print(Status.ACTIVE == 2) # False, plain Enum doesn't equal its raw value
That last line surprises people coming from other languages — a plain Enum member is not automatically equal to its underlying value. If I need that behavior, I use IntEnum or StrEnum instead (covered below).
Enum Variants: IntEnum, StrEnum, Flag, IntFlag
Python’s enum module provides several specialized base classes beyond plain Enum.
IntEnum
Members behave like actual integers, useful when interoperating with code expecting raw numbers:
from enum import IntEnum
class Priority(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
print(Priority.HIGH == 3) # True
print(Priority.HIGH > Priority.LOW) # True
print(Priority.HIGH + 1) # 4, behaves like an int
StrEnum (Python 3.11+)
Members behave like actual strings:
from enum import StrEnum
class Color(StrEnum):
RED = "red"
GREEN = "green"
BLUE = "blue"
print(Color.RED == "red") # True
print(f"Selected: {Color.RED}") # Selected: red
For earlier Python versions, I achieve the same effect by inheriting from both str and Enum:
class Color(str, Enum):
RED = "red"
GREEN = "green"
Flag and IntFlag
For combinable bitmask-style options, covered in more depth in bitwise-operator contexts:
from enum import Flag, auto
class Permission(Flag):
READ = auto()
WRITE = auto()
EXECUTE = auto()
combined = Permission.READ | Permission.WRITE
print(combined) # Permission.READ|WRITE
print(Permission.READ in combined) # True
Using auto() for Automatic Values
When the actual numeric or string value doesn’t matter, auto() saves me from manually assigning them:
from enum import Enum, auto
class Direction(Enum):
NORTH = auto()
SOUTH = auto()
EAST = auto()
WEST = auto()
print(Direction.NORTH.value) # 1
print(Direction.WEST.value) # 4
By default auto() generates sequential integers starting at 1, but this behavior can be customized by overriding _generate_next_value_.
Adding Methods and Properties to Enums
Enums are real classes, which means I can attach methods, properties, and even custom __init__ logic:
from enum import Enum
class Planet(Enum):
MERCURY = (3.3e23, 2.4e6)
VENUS = (4.9e24, 6.1e6)
EARTH = (5.9e24, 6.4e6)
def __init__(self, mass, radius):
self.mass = mass
self.radius = radius
def surface_gravity(self):
G = 6.674e-11
return G * self.mass / (self.radius ** 2)
print(Planet.EARTH.surface_gravity()) # ~9.8
This pattern — attaching computed behavior directly to enum members — is one of my favorite features, since it keeps related data and logic together instead of scattered across lookup dictionaries.
Enums With Aliases
Multiple names can point to the same value, creating aliases. By default, Python treats the second name as an alias for the first, not a distinct member:
class Status(Enum):
ACTIVE = 1
ENABLED = 1 # alias for ACTIVE
print(Status.ACTIVE is Status.ENABLED) # True
print(list(Status)) # [<Status.ACTIVE: 1>] — ENABLED doesn't appear separately
If I want to explicitly disallow accidental aliasing, I use the @unique decorator:
from enum import Enum, unique
@unique
class Status(Enum):
ACTIVE = 1
ENABLED = 1 # Raises ValueError: duplicate values found
How Enums Work Internally
Under the hood, Enum uses a metaclass (EnumMeta/EnumType) that intercepts class creation to transform simple class attributes into Enum member instances, ensures each member is a singleton, and wires up the lookup machinery (Status["NAME"], Status(value)) automatically.
Each enum member is created exactly once and cached, which is why identity comparison (is) works reliably — unlike regular class instances, calling Status(1) twice returns the same object both times rather than creating a new one.
print(Status(1) is Status(1)) # True
This singleton behavior also means enum members are memory-efficient regardless of how many times they’re referenced throughout a codebase.
Real-World Use Cases
API status codes:
class HTTPStatus(Enum):
OK = 200
NOT_FOUND = 404
SERVER_ERROR = 500
def handle_response(code):
status = HTTPStatus(code)
if status == HTTPStatus.OK:
print("Success")
elif status == HTTPStatus.NOT_FOUND:
print("Resource missing")
State machines:
class OrderState(Enum):
CREATED = auto()
PAID = auto()
SHIPPED = auto()
DELIVERED = auto()
VALID_TRANSITIONS = {
OrderState.CREATED: {OrderState.PAID},
OrderState.PAID: {OrderState.SHIPPED},
OrderState.SHIPPED: {OrderState.DELIVERED},
}
def transition(current, target):
if target not in VALID_TRANSITIONS.get(current, set()):
raise ValueError(f"Cannot go from {current} to {target}")
return target
Configuration options in automation scripts:
class LogLevel(StrEnum):
DEBUG = "debug"
INFO = "info"
WARNING = "warning"
ERROR = "error"
def configure_logging(level: LogLevel):
print(f"Logging configured at {level} level")
Best Practices
- Use
Enumfor general symbolic constants,IntEnum/StrEnumonly when interoperability with raw ints/strings is genuinely needed. - Use
auto()when the specific underlying value doesn’t matter, to reduce manual bookkeeping and merge conflicts. - Use
@uniquewhen accidental aliasing would be a bug rather than an intentional feature. - Attach behavior directly to enum members via methods rather than external lookup dictionaries, when that behavior is intrinsic to the member.
Common Mistakes
# Mistake: assuming plain Enum members equal their raw values
class Status(Enum):
ACTIVE = 1
print(Status.ACTIVE == 1) # False! Use IntEnum if this comparison is needed
# Mistake: expecting duplicate values to create distinct members
class Bad(Enum):
A = 1
B = 1
print(list(Bad)) # Only [<Bad.A: 1>], B is an alias
Debugging Tips
- Use
list(MyEnum)to see all actual (non-alias) members. - Use
MyEnum.__members__to see all names, including aliases, mapped to their members. - When a lookup fails, check whether you’re passing a value (
MyEnum(value)) versus a name (MyEnum["NAME"]) — mixing these up is a very commonValueError/KeyErrorsource.
FAQs
What’s the difference between Enum and IntEnum? Plain Enum members don’t compare equal to their raw underlying value; IntEnum members behave like actual integers and do compare equal to matching int values.
Can enum values be non-numeric? Yes — enum values can be strings, tuples, or any hashable value, not just integers.
How do I iterate over all enum members? Use for member in MyEnum: — this iterates in definition order and skips aliases.
Are enum members singletons? Yes — calling MyEnum(value) or MyEnum["NAME"] multiple times always returns the exact same object, enabling reliable is comparisons.
Summary
Python’s enum module turns loose collections of related constants into a proper, type-safe construct. Between plain Enum, IntEnum, StrEnum, and Flag/IntFlag, there’s a variant suited to nearly every use case I’ve run into — status codes, state machines, configuration options, and bitmask-style flags. Since adopting enums over raw string/int constants, entire categories of typo-driven bugs have simply stopped happening in my code.