Python Enums
Learn Python enums: create Enum, IntEnum, Flag, and auto() members, add methods, compare safely, and replace fragile magic numbers in your code.
An enum (short for enumeration) is a set of named, constant values grouped under a single type. Instead of scattering bare integers or strings like 1, 2, "pending", "active" through your code, you give each one a descriptive name — Status.PENDING, Color.RED — and Python guarantees that name always maps to the same value.
This chapter covers:
- Why enums exist and what problems they solve
- Creating an enum with the
Enumclass - Accessing members by name and by value
- Iterating over an enum
auto()— letting Python assign values for youIntEnum— enums that behave like integersFlag— combinable bit-flag enums- Adding methods and properties to an enum
- Aliases,
@unique, and_missing_ - When to use enums vs. other patterns
Before reading this chapter, make sure you are comfortable with Python classes and objects and Python data types.
Why Use Enums?
Consider this function that processes an order status passed as a plain integer:
def handle_order(status):
if status == 1:
print("Order is pending")
elif status == 2:
print("Order is active")
elif status == 3:
print("Order is complete")This works but has real problems:
- Magic numbers. What does
2mean in isolation? You have to trace back to the function definition. - No validation.
handle_order(99)silently does nothing — no error, no warning. - Typos are invisible.
handle_order(2)andhandle_order(20)are both valid Python. - Refactoring is risky. If you decide
1should mean something else, you have to find every1in the codebase.
Enums fix all of these. The same logic written with an enum is self-documenting, safe, and refactor-friendly:
from enum import Enum
class OrderStatus(Enum):
PENDING = 1
ACTIVE = 2
COMPLETE = 3
def handle_order(status: OrderStatus):
if status == OrderStatus.PENDING:
print("Order is pending")
elif status == OrderStatus.ACTIVE:
print("Order is active")
elif status == OrderStatus.COMPLETE:
print("Order is complete")
handle_order(OrderStatus.ACTIVE) # Order is activeThe intent is clear, and Python prevents handle_order(99) from accidentally matching any branch.
Creating an Enum
Import Enum from the enum module (part of the Python standard library — no installation needed) and subclass it:
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3Each class-level attribute (RED, GREEN, BLUE) becomes an enum member. The values on the right (1, 2, 3) can be integers, strings, or any other type — the choice is up to you.
Accessing members
There are three ways to reach an enum member:
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
# Attribute access (most common)
print(Color.RED) # Color.RED
# By name (square bracket notation)
print(Color['GREEN']) # Color.GREEN
# By value (call the class with the value)
print(Color(3)) # Color.BLUEEvery member exposes two attributes:
print(Color.RED.name) # RED
print(Color.RED.value) # 1Use .name when you need a human-readable label (for logging or display), and .value when you need to pass the underlying value to an external system (a database, an API).
repr and type
print(repr(Color.RED)) # <Color.RED: 1>
print(type(Color.RED)) # <enum 'Color'>An enum member is an instance of its enum class, not of int or str.
Iterating Over an Enum
Enums are iterable. Iteration yields members in definition order:
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
for color in Color:
print(color.name, color.value)
# RED 1
# GREEN 2
# BLUE 3You can also check membership:
print(Color.RED in Color) # TrueThis makes enums handy for populating drop-downs, building switch-like dispatch tables, or building lists of choices for user input.
auto() — Automatic Values
If the specific values do not matter — you only care that each member is distinct — use auto(). Python assigns sequential integers starting from 1:
from enum import Enum, auto
class Direction(Enum):
NORTH = auto()
SOUTH = auto()
EAST = auto()
WEST = auto()
for d in Direction:
print(d.name, d.value)
# NORTH 1
# SOUTH 2
# EAST 3
# WEST 4auto() is especially useful when the enum will grow over time and you do not want to manually renumber members.
Comparing Enum Members
Use is or == to compare members. Both work, but is is slightly faster because enum members are singletons — each name maps to exactly one object:
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
print(Color.RED is Color.RED) # True
print(Color.RED == Color.RED) # True
print(Color.RED == Color.GREEN) # FalseA plain Enum member does not equal its raw value:
print(Color.RED == 1) # FalseThis is intentional. It prevents accidental equality between different enums that share the same integer:
class Size(Enum):
SMALL = 1
print(Color.RED == Size.SMALL) # False — different typesIf you need value-based comparison (e.g., member > 1), use IntEnum instead (see below).
IntEnum — Enums That Behave Like Integers
IntEnum members are also regular Python integers. This means you can use arithmetic, comparison operators, and pass them anywhere an int is expected:
from enum import IntEnum
class Priority(IntEnum):
LOW = 1
MEDIUM = 2
HIGH = 3
print(Priority.HIGH > Priority.LOW) # True
print(Priority.MEDIUM + 10) # 12
print(Priority.HIGH == 3) # TrueA common use case is sorting a list of enum members:
from enum import IntEnum
class Level(IntEnum):
LOW = 1
MED = 2
HIGH = 3
levels = [Level.HIGH, Level.LOW, Level.MED]
print([l.name for l in sorted(levels)]) # ['LOW', 'MED', 'HIGH']When to prefer Enum over IntEnum
IntEnum's integer transparency is also its weakness: Priority.HIGH == 3 is True, so a mistyped literal 3 will silently compare equal to Priority.HIGH. Use plain Enum whenever you want strict type safety, and use IntEnum only when you genuinely need integer arithmetic or must interface with an API that deals in raw numbers.
Flag — Combinable Bit-Flag Enums
Flag is designed for scenarios where multiple options can be active at the same time. Its members are powers of two, and you combine them with the | (bitwise OR) operator:
from enum import Flag, auto
class Permission(Flag):
READ = auto()
WRITE = auto()
EXECUTE = auto()
ALL = READ | WRITE | EXECUTE
user = Permission.READ | Permission.WRITE
print(user) # Permission.WRITE|READ
print(Permission.READ in user) # True
print(Permission.EXECUTE in user) # Falseauto() inside Flag assigns successive powers of two (1, 2, 4, 8, …) so that combining members with | never produces ambiguous results.
Use Flag for permission systems, feature toggles, and any situation where you need a compact set of boolean switches.
Adding Methods and Properties
Because an enum is a class, you can add methods and properties to it. This keeps related logic inside the type rather than scattered across if/elif chains:
from enum import Enum
class HttpStatus(Enum):
OK = 200
CREATED = 201
NOT_FOUND = 404
INTERNAL_ERROR = 500
@property
def is_success(self):
return 200 <= self.value < 300
@property
def is_error(self):
return self.value >= 400
def handle_response(status: HttpStatus):
if status.is_success:
print(f"{status.value} {status.name}: request succeeded")
elif status.is_error:
print(f"{status.value} {status.name}: request failed")
handle_response(HttpStatus.OK) # 200 OK: request succeeded
handle_response(HttpStatus.NOT_FOUND) # 404 NOT_FOUND: request failedYou can also give an enum a custom __init__ to store extra data per member. Provide the values as tuples:
from enum import Enum
class Planet(Enum):
MERCURY = (3.303e+23, 2.4397e6)
VENUS = (4.869e+24, 6.0518e6)
EARTH = (5.976e+24, 6.37814e6)
def __init__(self, mass, radius):
self.mass = mass
self.radius = radius
@property
def surface_gravity(self):
G = 6.67430e-11
return G * self.mass / (self.radius ** 2)
print(round(Planet.EARTH.surface_gravity, 2)) # 9.8
print(round(Planet.MERCURY.surface_gravity, 2)) # 3.7The (mass, radius) tuple becomes the constructor arguments; self.value still holds the full tuple.
Aliases and @unique
If two members share the same value, the second becomes an alias — it resolves to the first member. Aliases are not yielded during iteration:
from enum import Enum
class Status(Enum):
ACTIVE = 1
RUNNING = 1 # alias for ACTIVE
print(Status.ACTIVE is Status.RUNNING) # True
print(list(Status)) # [<Status.ACTIVE: 1>]Aliases are occasionally useful (e.g., a legacy name pointing to a new one), but they can also mask typos. Apply the @unique decorator to forbid duplicate values entirely:
from enum import Enum, unique
@unique
class Status(Enum):
PENDING = 1
ACTIVE = 2
INACTIVE = 3
# Trying to add a duplicate value to a @unique enum raises ValueError:
# ValueError: duplicate values found in <enum 'Bad'>: B -> A@unique is a good default for any enum where accidental aliasing would be a bug.
Custom Lookup with _missing_
By default, calling Color('unknown') raises a ValueError. You can override the class method _missing_ to handle unrecognised values — for example, to do a case-insensitive lookup:
from enum import Enum
class Color(Enum):
RED = 'red'
GREEN = 'green'
BLUE = 'blue'
@classmethod
def _missing_(cls, value):
if isinstance(value, str):
for member in cls:
if member.value == value.lower():
return member
return None
print(Color('RED')) # Color.RED
print(Color('Green')) # Color.GREEN_missing_ receives the value that was not found. Return the matching member, or None (which lets Python raise its default ValueError).
When to Use Enums
Enums are the right choice when:
- A variable can only hold one of a fixed set of named states (order status, HTTP verb, card suit).
- You want to prevent invalid values from silently passing through.
- The same concept is compared in multiple places and you want a single source of truth.
- You need to iterate over all valid values (populating a form, documenting an API).
You probably do not need an enum when:
- The set of values is open-ended or changes at runtime (use a dictionary or a database lookup table).
- You only need two states —
True/Falsewith a clear boolean meaning is simpler. - The values come from user input that must be validated against a schema — consider a library like Pydantic, which integrates neatly with Python enums.
For closely related patterns, see Python dataclasses (for structured data with default values) and Python abstract classes (for enforcing interface contracts across subclasses). If you need named constant containers without the full enum machinery, the Python collections module offers namedtuple as an alternative.