Magic numbers and loose string constants lurking in your code create bugs that are hard to trace and even harder to maintain. Python's enum module solves this by letting you define fixed sets of named constants that are readable, type-safe, and impossible to accidentally reassign. This guide covers everything from basic Enum creation to advanced patterns with Flag, StrEnum, and the verify decorator.
The enum module has been part of Python's standard library since version 3.4, and it has continued to evolve through subsequent releases. Python 3.11 introduced StrEnum, ReprEnum, and the verify decorator. Python 3.13 refined how auto() calculates next values and added the _member_names_ attribute. Whether you are writing a small script or architecting a large application, enumerations bring structure, clarity, and safety to any codebase that relies on fixed sets of values.
What Is an Enum and Why Use One
An enumeration is a set of symbolic names, called members, that are bound to unique, constant values. Think of it as a way to give meaningful labels to values that would otherwise be anonymous integers or scattered strings. Instead of writing status = 1 somewhere and hoping everyone remembers that 1 means "pending," you write status = OrderStatus.PENDING and the intent becomes self-documenting.
Enumerations provide several advantages over plain constants. Members are immutable, which means they cannot be reassigned or modified after the class is created. They are singletons, so comparing two references to the same member with is always returns True. They support iteration, letting you loop over every defined value in the enum. And they produce clear, readable output when printed or logged, because repr() and str() include the class name and member name by default.
Enum members evaluate to True in boolean contexts, even if their value is 0. This is different from regular integers and is an intentional design decision. If you need integer-like truthiness behavior, use IntEnum instead.
Creating Your First Enum
Creating an enum starts with importing the Enum class and defining a new class that inherits from it. Each class attribute you assign becomes a member of the enumeration.
from enum import Enum
class Color(Enum):
RED = 1
GREEN = 2
BLUE = 3
Once defined, you access members as attributes of the class. Every member has two core properties: name, which is the string name you gave it, and value, which is whatever you assigned to it.
# Access a member
print(Color.RED) # Color.RED
print(Color.RED.name) # RED
print(Color.RED.value) # 1
# Look up by value
print(Color(2)) # Color.GREEN
# Look up by name
print(Color["BLUE"]) # Color.BLUE
Enums also support iteration and membership testing, which makes them convenient for validation logic and building user-facing menus or dropdowns.
# Iterate over all members
for color in Color:
print(f"{color.name} = {color.value}")
# Membership testing
print(Color.RED in Color) # True
The Functional API
If you prefer a more compact syntax, Python lets you create enums using a functional call. This is useful when you are generating enums dynamically or want a quick one-liner.
Color = Enum("Color", ["RED", "GREEN", "BLUE"])
# Values are automatically assigned starting at 1
print(Color.RED.value) # 1
print(Color.GREEN.value) # 2
The convention is to use UPPER_CASE names for enum members. This makes them visually distinct from regular class attributes and methods, and aligns with how constants are typically named in Python.
auto() and Controlling Member Values
Manually assigning a value to every member can become tedious, especially when the specific values do not matter. The auto() function handles this by generating values for you automatically.
from enum import Enum, auto
class Priority(Enum):
LOW = auto()
MEDIUM = auto()
HIGH = auto()
CRITICAL = auto()
print(Priority.LOW.value) # 1
print(Priority.CRITICAL.value) # 4
By default, auto() starts at 1 and increments by 1 for each subsequent member. The reason it starts at 1 rather than 0 is that 0 evaluates to False in a boolean context, and enum members are designed to always be truthy.
Customizing auto() with _generate_next_value_
You can override the _generate_next_value_ static method to change how auto() produces values. A common pattern is to use the lowercased member name as the value instead of an integer.
class HttpMethod(Enum):
@staticmethod
def _generate_next_value_(name, start, count, last_values):
return name.lower()
GET = auto()
POST = auto()
PUT = auto()
DELETE = auto()
print(HttpMethod.GET.value) # get
print(HttpMethod.DELETE.value) # delete
Starting in Python 3.13, auto() always uses the highest existing member value incremented by 1, rather than the last-seen value. This makes behavior more predictable when members are defined out of numeric order. If any member has an incompatible type, auto() will raise an error.
IntEnum, StrEnum, and Other Specialized Types
The base Enum class keeps its members isolated from other types. You cannot compare a Color.RED member to the integer 1 and get True. This is a feature, not a limitation -- it prevents accidental comparisons that would be meaningless. However, there are legitimate cases where you want enum members to behave like their underlying primitive type.
IntEnum
IntEnum members are full subclasses of int. They can be used in arithmetic, comparisons with plain integers, and anywhere an integer is expected.
from enum import IntEnum
class HttpStatus(IntEnum):
OK = 200
NOT_FOUND = 404
SERVER_ERROR = 500
# Behaves like an integer
print(HttpStatus.OK == 200) # True
print(HttpStatus.NOT_FOUND + 1) # 405
print(HttpStatus.OK < HttpStatus.NOT_FOUND) # True
When you perform arithmetic on an IntEnum member, the result loses its enumeration status and becomes a plain int. For example, HttpStatus.OK + 1 returns 201, not a member of HttpStatus. Use IntEnum only when you genuinely need integer compatibility.
StrEnum
StrEnum was introduced in Python 3.11 and works just like IntEnum but for strings. Members are both enum members and str subclasses, so they work seamlessly in string operations.
from enum import StrEnum
class ContentType(StrEnum):
JSON = "application/json"
HTML = "text/html"
PLAIN = "text/plain"
# Use directly in string contexts
headers = {"Content-Type": ContentType.JSON}
print(headers) # {'Content-Type': 'application/json'}
# String comparisons work
print(ContentType.JSON == "application/json") # True
When you use auto() with StrEnum, the generated value is the lowercased version of the member name, rather than an incrementing integer.
class Env(StrEnum):
DEVELOPMENT = auto()
STAGING = auto()
PRODUCTION = auto()
print(Env.DEVELOPMENT.value) # development
print(Env.PRODUCTION.value) # production
Flag and IntFlag for Bitwise Operations
When you need to combine multiple options into a single value using bitwise operators, Flag and IntFlag are the right tools. Each member represents a single bit, and you can combine them with | (OR), & (AND), and ~ (NOT).
from enum import Flag, auto
class Permission(Flag):
READ = auto()
WRITE = auto()
EXECUTE = auto()
# Combine permissions
user_perms = Permission.READ | Permission.WRITE
print(user_perms) # Permission.READ|WRITE
print(Permission.READ in user_perms) # True
print(Permission.EXECUTE in user_perms) # False
With Flag, auto() assigns powers of two (1, 2, 4, 8, ...) rather than sequential integers. This ensures each member occupies a single bit in the combined value.
You can also predefine common combinations as members of the same enum.
class Permission(Flag):
READ = auto()
WRITE = auto()
EXECUTE = auto()
# Composite members
READ_WRITE = READ | WRITE
ALL = READ | WRITE | EXECUTE
admin_perms = Permission.ALL
print(Permission.WRITE in admin_perms) # True
IntFlag works the same way but also inherits from int, so combined values can be used in integer contexts. Choose Flag when you want strict type safety and IntFlag when you need backward compatibility with integer-based APIs.
Aliases, @unique, and the verify Decorator
By default, Python allows multiple names to share the same value. When this happens, the first name defined becomes the canonical member and any subsequent names with the same value become aliases.
class Shape(Enum):
SQUARE = 2
DIAMOND = 1
CIRCLE = 3
ALIAS_FOR_SQUARE = 2 # This is an alias
print(Shape.ALIAS_FOR_SQUARE is Shape.SQUARE) # True
# Aliases are excluded from iteration
print(list(Shape))
# [<Shape.SQUARE: 2>, <Shape.DIAMOND: 1>, <Shape.CIRCLE: 3>]
If you want to prevent aliases entirely, use the @unique decorator. It raises a ValueError at class creation time if any two members share a value.
from enum import Enum, unique
@unique
class Direction(Enum):
NORTH = 1
SOUTH = 2
EAST = 3
WEST = 4
# UP = 1 <-- would raise ValueError
The verify Decorator
Python 3.11 introduced the verify decorator, which provides more flexible validation than @unique alone. It accepts one or more check options from the EnumCheck enum.
from enum import Enum, verify, UNIQUE, CONTINUOUS, NAMED_FLAGS
@verify(UNIQUE)
class Season(Enum):
SPRING = 1
SUMMER = 2
AUTUMN = 3
WINTER = 4
@verify(CONTINUOUS)
class Grade(Enum):
A = 1
B = 2
C = 3
# D = 5 <-- would raise ValueError: missing value 4
UNIQUE enforces the same constraint as the @unique decorator. CONTINUOUS ensures there are no gaps between the lowest and highest integer values. NAMED_FLAGS ensures that any composite flags are built only from individually named members.
Advanced Patterns and Custom Methods
Enum classes are full Python classes, which means you can add methods, properties, and even custom __init__ logic to them.
Adding Methods to Enums
class Planet(Enum):
MERCURY = (3.303e+23, 2.4397e6)
VENUS = (4.869e+24, 6.0518e6)
EARTH = (5.976e+24, 6.37814e6)
MARS = (6.421e+23, 3.3972e6)
def __init__(self, mass, radius):
self.mass = mass # in kilograms
self.radius = radius # in meters
@property
def surface_gravity(self):
G = 6.67300e-11 # universal gravitational constant
return G * self.mass / (self.radius * self.radius)
print(Planet.EARTH.surface_gravity) # 9.802652743337129
print(Planet.MARS.mass) # 6.421e+23
This pattern is powerful for building domain-specific enumerations where each member carries structured data along with its identity.
Using Mixins
You can mix in other types by placing them before Enum in the class definition. This gives members the behavior of both the mixin type and the enum.
class OrderedStrEnum(str, Enum):
"""Members are comparable strings with ordering."""
def __lt__(self, other):
if self.__class__ is other.__class__:
members = list(self.__class__)
return members.index(self) < members.index(other)
return NotImplemented
class Size(OrderedStrEnum):
SMALL = "S"
MEDIUM = "M"
LARGE = "L"
XLARGE = "XL"
print(Size.SMALL < Size.LARGE) # True
print(Size.MEDIUM == "M") # True
The _missing_ Hook
Override _missing_ to intercept value lookups that do not match any member. This is useful for case-insensitive matching or mapping legacy values.
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
@classmethod
def _missing_(cls, value):
for member in cls:
if member.value == value.lower():
return member
return None
print(Color("RED")) # Color.RED
print(Color("Green")) # Color.GREEN
Real-World Use Cases
Enumerations are not just an academic exercise. They appear throughout production Python code in patterns like these.
State Machines
Enums are a natural fit for representing states and transitions. Defining allowed states as enum members means invalid states become impossible to construct.
class OrderStatus(Enum):
PENDING = auto()
CONFIRMED = auto()
SHIPPED = auto()
DELIVERED = auto()
CANCELLED = auto()
def can_transition_to(self, new_status):
transitions = {
OrderStatus.PENDING: {OrderStatus.CONFIRMED, OrderStatus.CANCELLED},
OrderStatus.CONFIRMED: {OrderStatus.SHIPPED, OrderStatus.CANCELLED},
OrderStatus.SHIPPED: {OrderStatus.DELIVERED},
OrderStatus.DELIVERED: set(),
OrderStatus.CANCELLED: set(),
}
return new_status in transitions[self]
order = OrderStatus.PENDING
print(order.can_transition_to(OrderStatus.CONFIRMED)) # True
print(order.can_transition_to(OrderStatus.DELIVERED)) # False
Configuration and Feature Flags
class Feature(Flag):
DARK_MODE = auto()
NOTIFICATIONS = auto()
BETA_FEATURES = auto()
ADMIN_PANEL = auto()
DEFAULT = DARK_MODE | NOTIFICATIONS
user_features = Feature.DEFAULT | Feature.BETA_FEATURES
print(Feature.DARK_MODE in user_features) # True
print(Feature.ADMIN_PANEL in user_features) # False
Database and API Integration
StrEnum members work directly as string values in dictionaries, JSON payloads, and database queries without requiring any manual .value access.
import json
class Role(StrEnum):
ADMIN = "admin"
EDITOR = "editor"
VIEWER = "viewer"
# Serializes cleanly to JSON
payload = {"user": "alice", "role": Role.ADMIN}
print(json.dumps(payload))
# {"user": "alice", "role": "admin"}
Key Takeaways
- Use Enum for type-safe constants: Whenever you find yourself defining a group of related constants as plain integers or strings, reach for
Enuminstead. You get immutability, identity-based comparison, iteration, and clearrepr()output for free. - Pick the right subclass for interoperability: Use
IntEnumwhen you need integer compatibility,StrEnumwhen you need string compatibility, andFlag/IntFlagwhen you need to combine members with bitwise operators. Use the baseEnumclass when you want strict isolation from other types. - Leverage auto() and verify for cleaner code: Let
auto()handle value assignment when specific values do not matter. Use the@verifydecorator withUNIQUE,CONTINUOUS, orNAMED_FLAGSto catch structural problems at class creation time rather than at runtime. - Add behavior to your enums: Enum classes support custom methods, properties,
__init__with tuple unpacking, and hooks like_missing_. This turns them into rich domain objects rather than flat lookups, making them powerful tools for patterns like state machines and configuration management.
Python's enum module transforms what would otherwise be fragile, scattered constants into structured, self-documenting types. Start using enumerations wherever you have a fixed set of values, and you will find that bugs decrease, code reviews go faster, and your intent becomes clearer for everyone who reads your code.