Intermediate
If you have spent time debugging Python code, you have probably encountered the silent killers of software reliability: magic strings and magic numbers. A developer writes status = "active" in one function but checks if status == "Active" (capital A) in another. A bug is born. Days later, when the mismatch surfaces in production, your fingers itch to throttle the typo. Python enums exist precisely to prevent this nightmare. They transform those fragile string values into type-safe, self-documenting objects that your IDE can help you complete and your type checker can validate before a single line runs.
The good news: enums are built into Python’s standard library. No external packages needed, no complex installation steps. They integrate seamlessly with the language and work beautifully with type hints, making your code cleaner and more maintainable. Enums are also more than just a neat organizational trick — they are a best practice embraced by the Python community and used in production code across the industry, from web frameworks to data science pipelines.
In this tutorial, we will explore what enums are, why they matter, and how to use them effectively. We will start with the basics, move through automatic value generation, then advance to string enums, flags, and real-world patterns like state machines. By the end, you will understand when to reach for enums and how to wield them to write code that is both safer and more expressive.

Defining Your First Enum
Python’s enum module lives in the standard library — no install needed. The simplest enum is a subclass of Enum with class-level attributes for each member:
from enum import Enum
class Status(Enum):
ACTIVE = "active"
INACTIVE = "inactive"
SUSPENDED = "suspended"
# Use members anywhere a status is expected
user_status = Status.ACTIVE
print(user_status) # Status.ACTIVE
print(user_status.name) # 'ACTIVE'
print(user_status.value) # 'active'
# Iteration walks the members in definition order
for status in Status:
print(status.name, status.value)
Three things to notice. First, members are accessed by attribute (Status.ACTIVE), not by string lookup. A typo like Status.ACTIV raises AttributeError at import time, not at runtime. Second, each member has both a name (the attribute identifier) and a value (whatever you assigned). Third, the enum class is iterable — you can loop over its members for dropdowns, validation, or any other “list all options” use case.
Auto-Generated Values with auto()
When you don’t care what the underlying value is — only that members are distinct — use auto() to let Python assign them:
from enum import Enum, auto
class HttpMethod(Enum):
GET = auto()
POST = auto()
PUT = auto()
DELETE = auto()
print(HttpMethod.GET.value) # 1
print(HttpMethod.POST.value) # 2
By default auto() generates integers starting at 1. To customize the sequence, override _generate_next_value_:
class CssClass(Enum):
def _generate_next_value_(name, start, count, last_values):
return name.lower().replace('_', '-')
PRIMARY = auto() # 'primary'
PRIMARY_LIGHT = auto() # 'primary-light'
String Enums and IntEnum
Sometimes you want the enum to also behave like its underlying type — so Status.ACTIVE == "active" returns True and the value serializes cleanly to JSON. Use StrEnum (Python 3.11+) for strings or IntEnum for integers:
from enum import StrEnum, IntEnum
class Priority(IntEnum):
LOW = 1
MEDIUM = 5
HIGH = 10
class Color(StrEnum):
RED = "red"
GREEN = "green"
BLUE = "blue"
# IntEnum behaves like int
print(Priority.HIGH > Priority.LOW) # True
print(Priority.HIGH + 5) # 15
# StrEnum behaves like str
print(Color.RED == "red") # True
print(Color.GREEN.upper()) # 'GREEN'
import json
print(json.dumps({"color": Color.RED})) # '{"color": "red"}' — clean serialization
This is the practical choice for API payloads, database column values, and anything that crosses a serialization boundary. The enum gives you type-safety inside your code; the underlying type gives you painless JSON / SQL / config-file output.
Bit Flags with Flag and IntFlag
For “any combination of these options” — file permissions, feature toggles, network capabilities — use Flag (or IntFlag for compatibility with bitwise int operations). Members can be ORed together to form combinations:
from enum import Flag, auto
class Permission(Flag):
READ = auto()
WRITE = auto()
EXECUTE = auto()
ADMIN = READ | WRITE | EXECUTE # combination shortcut
# Combine at use site
user_perms = Permission.READ | Permission.WRITE
print(Permission.READ in user_perms) # True
print(Permission.EXECUTE in user_perms) # False
print(bool(user_perms & Permission.READ)) # True
Bit flags are 100x more readable than raw integers (perms = 0b110) and 10x more readable than dictionaries of bools ({"read": True, "write": True}). Anywhere you’d reach for a set of strings as a “kind of bitfield”, reach for Flag instead.
A Real Example: State Machine
Enums shine when modeling state transitions. Here’s an order-status state machine that rejects illegal transitions at the type level:
from enum import Enum
class OrderStatus(Enum):
DRAFT = "draft"
PENDING = "pending"
PAID = "paid"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
ALLOWED_TRANSITIONS = {
OrderStatus.DRAFT: {OrderStatus.PENDING, OrderStatus.CANCELLED},
OrderStatus.PENDING: {OrderStatus.PAID, OrderStatus.CANCELLED},
OrderStatus.PAID: {OrderStatus.SHIPPED, OrderStatus.CANCELLED},
OrderStatus.SHIPPED: {OrderStatus.DELIVERED},
OrderStatus.DELIVERED: set(),
OrderStatus.CANCELLED: set(),
}
def transition(current: OrderStatus, target: OrderStatus) -> OrderStatus:
if target not in ALLOWED_TRANSITIONS[current]:
raise ValueError(f"Illegal transition: {current.value} -> {target.value}")
return target
# Usage
state = OrderStatus.DRAFT
state = transition(state, OrderStatus.PENDING) # OK
state = transition(state, OrderStatus.PAID) # OK
# state = transition(state, OrderStatus.DRAFT) # raises ValueError
The enum + transition map combo gives you a self-documenting workflow that’s impossible to misuse. Add type hints (state: OrderStatus) and mypy will catch any caller that tries to pass a raw string.
Common Pitfalls
- Enums aren’t tuples.
Status.ACTIVEis not a string, even though it might display like one. Use.valuewhen you need the underlying primitive, or pickStrEnum/IntEnumto get implicit type compatibility. - Singleton equality. Two members with the same value collapse into a single member by default.
class X(Enum): A = 1; B = 1makesX.Ban alias forX.A. If you want distinct members with the same value, you need a workaround (custom__init__or a unique discriminator). - JSON serialization.
json.dumps(Status.ACTIVE)raisesTypeErrorfor plainEnumbut works forStrEnum/IntEnum. Either subclassStrEnumor write a customdefault=handler. - Comparison with strings.
Status.ACTIVE == "active"isFalsefor a plainEnum(different types) butTrueforStrEnum. Pick the right base class up front to avoid surprises. - Don’t subclass an enum that already has members. Python forbids it: once you define members, the class is closed for extension. Refactor to a common base or use composition.
FAQ
Q: Should I use a plain Enum, IntEnum, or StrEnum?
A: StrEnum for API/JSON values where the string matters externally. IntEnum for bitwise math or legacy integer-coded statuses. Plain Enum for internal-only values where the underlying type shouldn’t leak into comparisons.
Q: How do I get an enum member from its value?
A: Call the enum like a function: Status("active") returns Status.ACTIVE. Wrap in try/except ValueError for unknown values, or use Status._value2member_map_.get(...) for a no-exception lookup.
Q: Can enum members have extra attributes or methods?
A: Yes — enums are real classes. Override __init__ to attach data, and add regular methods on the class body. A common pattern is class Country(Enum): US = ("United States", "USD"); GB = ("United Kingdom", "GBP") with each member carrying a tuple of attributes.
Q: Are enums faster than string constants?
A: Marginally. The real win is correctness, not speed. Avoid bench-marking enums against strings — you’ll measure noise.
Q: How do mypy / Pyright / Pylance handle enums?
A: They love them. Type-narrowing with if status is Status.ACTIVE: works correctly, and unknown member access is caught at lint time. Enums are one of Python’s most type-checker-friendly features.
Wrapping Up
Reach for an enum any time you have a fixed, named set of values — order statuses, user roles, HTTP methods, log levels, permissions. Plain Enum for internal identifiers, StrEnum for JSON-friendly values, IntEnum for bit math, Flag for combinations. The cost is one extra class definition; the payoff is a typo never silently breaks production.
For the complete reference, see the official Python enum documentation.
Related Python Tutorials
Continue learning with these related guides: