I used to scatter magic strings and numbers throughout my code. "pending" here, "completed" there, status == 1 somewhere else. Then I learned about enums, and my code got a lot cleaner. Here's everything I've figured out about organizing constants in Python.
The Problem with Raw Constants
Early in my Python journey, I'd define constants like this:
# constants.py
STATUS_PENDING = "pending"
STATUS_PROCESSING = "processing"
STATUS_COMPLETED = "completed"
STATUS_FAILED = "failed"
PRIORITY_LOW = 1
PRIORITY_MEDIUM = 2
PRIORITY_HIGH = 3Then use them:
from constants import STATUS_PENDING, STATUS_COMPLETED
task = {"status": STATUS_PENDING}
# Later...
if task["status"] == STATUS_COMPLETED:
notify_user()This works, but has problems:
- No grouping: Related constants are just variables floating around
- No validation: Nothing stops you from using
"compelted"(typo) - No IDE help: Autocomplete shows every constant, not just valid statuses
- No iteration: You can't easily list all valid statuses
Enter the Enum Class
Python's Enum groups related constants together:
from enum import Enum
class Status(Enum):
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
FAILED = "failed"Now you have a proper type:
# Use enum members
task_status = Status.PENDING
# Compare properly
if task_status == Status.COMPLETED:
notify_user()
# Get the value if needed
print(task_status.value) # "pending"
# Get the name
print(task_status.name) # "PENDING"The IDE now knows Status. should autocomplete to PENDING, PROCESSING, etc. Typos become impossible—Status.COMPELTED raises AttributeError immediately.
Enum Identity vs Equality
Enums use identity comparison by default:
# These are the same object
print(Status.PENDING is Status.PENDING) # True
# Equality works too
print(Status.PENDING == Status.PENDING) # True
# But comparing to raw values doesn't work!
print(Status.PENDING == "pending") # False (this surprised me at first)That last one is important. Enums don't equal their values by default. You need .value or use StrEnum/IntEnum (more on those later).
Creating Enums from Values
When you have a value and need the enum:
# From value
status = Status("pending")
print(status) # Status.PENDING
# From name
status = Status["PENDING"]
print(status) # Status.PENDING
# Invalid values raise errors
Status("invalid") # ValueError: 'invalid' is not a valid StatusThis is great for parsing input—invalid values fail fast.
auto(): Let Python Assign Values
Sometimes you don't care about the actual values. Use auto():
from enum import Enum, auto
class Priority(Enum):
LOW = auto()
MEDIUM = auto()
HIGH = auto()
CRITICAL = auto()
print(Priority.LOW.value) # 1
print(Priority.MEDIUM.value) # 2
print(Priority.HIGH.value) # 3
print(Priority.CRITICAL.value) # 4auto() assigns incrementing integers starting from 1. The values themselves don't matter—you just care that Priority.HIGH is Priority.HIGH.
Customizing auto()
You can override how auto() generates values:
from enum import Enum, auto
class Color(Enum):
def _generate_next_value_(name, start, count, last_values):
return name.lower()
RED = auto()
GREEN = auto()
BLUE = auto()
print(Color.RED.value) # "red"
print(Color.GREEN.value) # "green"I don't use this often, but it's there if you need it.
IntEnum: When You Need Integer Behavior
Regular enums don't compare equal to their values. IntEnum changes that:
from enum import IntEnum
class HTTPStatus(IntEnum):
OK = 200
CREATED = 201
NOT_FOUND = 404
SERVER_ERROR = 500
# Compares to integers!
print(HTTPStatus.OK == 200) # True
# Can use in math (though you rarely should)
print(HTTPStatus.OK + 1) # 201
# Works in if statements with integers
response_code = 200
if response_code == HTTPStatus.OK:
print("Success!")When to Use IntEnum
I use IntEnum when:
- Interfacing with APIs that return integer codes
- Database fields store integers
- You need integer comparison
But be careful—IntEnum members can be used anywhere an int works, which can lead to weird bugs:
class Priority(IntEnum):
LOW = 1
HIGH = 2
# This works but is probably a bug
result = Priority.LOW + Priority.HIGH # 3 (an int, not a Priority!)StrEnum: The Modern Choice (Python 3.11+)
Python 3.11 added StrEnum for string-valued enums:
from enum import StrEnum
class Status(StrEnum):
PENDING = "pending"
PROCESSING = "processing"
COMPLETED = "completed"
# Compares to strings!
print(Status.PENDING == "pending") # True
# Works in f-strings naturally
print(f"Current status: {Status.PENDING}") # "Current status: pending"
# Can use anywhere a string is expected
status_upper = Status.PENDING.upper() # "PENDING"StrEnum is my go-to for status fields, API responses, and anything that gets serialized to JSON.
StrEnum Before Python 3.11
If you're stuck on older Python, you can create your own:
from enum import Enum
class StrEnum(str, Enum):
pass
class Status(StrEnum):
PENDING = "pending"
COMPLETED = "completed"
# Works the same way
print(Status.PENDING == "pending") # TrueFlag: Bitwise Operations
Flag is for when you need to combine options:
from enum import Flag, auto
class Permission(Flag):
READ = auto()
WRITE = auto()
DELETE = auto()
ADMIN = READ | WRITE | DELETE # Combined flag
# Combine permissions
user_perms = Permission.READ | Permission.WRITE
# Check permissions
print(Permission.READ in user_perms) # True
print(Permission.DELETE in user_perms) # False
# Add permission
user_perms |= Permission.DELETE
print(Permission.DELETE in user_perms) # True
# Remove permission
user_perms &= ~Permission.DELETE
print(Permission.DELETE in user_perms) # FalseFlag Values Are Powers of Two
When using auto() with Flag, values are powers of 2 for bitwise operations:
class Permission(Flag):
READ = auto() # 1 (0b001)
WRITE = auto() # 2 (0b010)
DELETE = auto() # 4 (0b100)
combined = Permission.READ | Permission.WRITE # 3 (0b011)IntFlag: When You Need Integer Behavior
Like IntEnum, IntFlag compares equal to integers:
from enum import IntFlag, auto
class Permission(IntFlag):
READ = auto()
WRITE = auto()
DELETE = auto()
# Can compare to ints
print(Permission.READ == 1) # True
print((Permission.READ | Permission.WRITE) == 3) # True
# Database storage
perms_int = int(Permission.READ | Permission.WRITE) # 3
perms_back = Permission(perms_int) # Permission.READ|Permission.WRITEI use IntFlag when storing permissions in a database as a single integer column.
Custom Values and Rich Enums
Enum values can be anything—not just strings or integers:
Tuple Values
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.67430e-11
return G * self.mass / (self.radius ** 2)
print(Planet.EARTH.mass) # 5.976e+24
print(Planet.EARTH.surface_gravity) # ~9.8Dict-like Values
class HTTPMethod(Enum):
GET = {"safe": True, "idempotent": True}
POST = {"safe": False, "idempotent": False}
PUT = {"safe": False, "idempotent": True}
DELETE = {"safe": False, "idempotent": True}
@property
def is_safe(self):
return self.value["safe"]
@property
def is_idempotent(self):
return self.value["idempotent"]
print(HTTPMethod.GET.is_safe) # True
print(HTTPMethod.POST.is_idempotent) # FalseMethods on Enums
Enums can have methods just like any class:
from enum import Enum
class TaskStatus(Enum):
DRAFT = "draft"
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
CANCELLED = "cancelled"
def is_active(self) -> bool:
"""Check if task is in an active state."""
return self in (TaskStatus.PENDING, TaskStatus.IN_PROGRESS)
def is_terminal(self) -> bool:
"""Check if task is in a final state."""
return self in (TaskStatus.COMPLETED, TaskStatus.CANCELLED)
def can_transition_to(self, new_status: "TaskStatus") -> bool:
"""Check if transition to new_status is valid."""
valid_transitions = {
TaskStatus.DRAFT: {TaskStatus.PENDING, TaskStatus.CANCELLED},
TaskStatus.PENDING: {TaskStatus.IN_PROGRESS, TaskStatus.CANCELLED},
TaskStatus.IN_PROGRESS: {TaskStatus.COMPLETED, TaskStatus.CANCELLED},
TaskStatus.COMPLETED: set(),
TaskStatus.CANCELLED: set(),
}
return new_status in valid_transitions[self]
# Usage
status = TaskStatus.PENDING
print(status.is_active()) # True
print(status.can_transition_to(TaskStatus.COMPLETED)) # False
print(status.can_transition_to(TaskStatus.IN_PROGRESS)) # TrueClass Methods on Enums
class Priority(Enum):
LOW = 1
MEDIUM = 2
HIGH = 3
CRITICAL = 4
@classmethod
def from_string(cls, s: str) -> "Priority":
"""Parse priority from various string formats."""
mapping = {
"low": cls.LOW, "l": cls.LOW, "1": cls.LOW,
"medium": cls.MEDIUM, "med": cls.MEDIUM, "m": cls.MEDIUM, "2": cls.MEDIUM,
"high": cls.HIGH, "h": cls.HIGH, "3": cls.HIGH,
"critical": cls.CRITICAL, "crit": cls.CRITICAL, "c": cls.CRITICAL, "4": cls.CRITICAL,
}
result = mapping.get(s.lower().strip())
if result is None:
raise ValueError(f"Unknown priority: {s}")
return result
# Flexible parsing
print(Priority.from_string("high")) # Priority.HIGH
print(Priority.from_string("H")) # Priority.HIGH
print(Priority.from_string("3")) # Priority.HIGHEnum Iteration
Enums are iterable—this is one of their best features:
class Color(Enum):
RED = "red"
GREEN = "green"
BLUE = "blue"
# Iterate over all members
for color in Color:
print(f"{color.name}: {color.value}")
# RED: red
# GREEN: green
# BLUE: blue
# List all values
all_values = [c.value for c in Color]
print(all_values) # ['red', 'green', 'blue']
# List all names
all_names = [c.name for c in Color]
print(all_names) # ['RED', 'GREEN', 'BLUE']
# Convert to choices (useful for forms/APIs)
choices = [(c.value, c.name.title()) for c in Color]
print(choices) # [('red', 'Red'), ('green', 'Green'), ('blue', 'Blue')]Useful Iteration Patterns
class Status(Enum):
DRAFT = "draft"
PENDING = "pending"
ACTIVE = "active"
COMPLETED = "completed"
# Check if value is valid
def is_valid_status(value: str) -> bool:
return value in [s.value for s in Status]
# Get enum by value (safe)
def get_status(value: str) -> Status | None:
for status in Status:
if status.value == value:
return status
return None
# Random enum member (useful for testing)
import random
random_status = random.choice(list(Status))members: The Full Dictionary
Every enum has a __members__ attribute that's a dict:
print(Status.__members__)
# {'DRAFT': <Status.DRAFT: 'draft'>, 'PENDING': <Status.PENDING: 'pending'>, ...}
# Useful for validation
if "DRAFT" in Status.__members__:
print("Valid member name")Enum vs Constants Module: When to Use Each
I still use constants modules sometimes. Here's how I decide:
Use Enums When:
- Values are related and form a closed set
- Type safety matters—you want to prevent invalid values
- You need iteration over all valid values
- IDE support is valuable (autocomplete, type hints)
- Values have behavior (methods, computed properties)
# Good enum use case: task statuses
class TaskStatus(Enum):
PENDING = "pending"
COMPLETED = "completed"Use Constants Module When:
- Values are independent configuration settings
- Values change between environments (dev/prod)
- Values are used for external systems (API keys, URLs)
- Simple is better—no need for type checking
# constants.py - good for configuration
DATABASE_URL = "postgresql://localhost/mydb"
API_TIMEOUT = 30
MAX_RETRIES = 3
CACHE_TTL = 3600
# Also good: unrelated magic numbers with clear names
SECONDS_PER_MINUTE = 60
BYTES_PER_KB = 1024
DEFAULT_PAGE_SIZE = 50The Middle Ground: Typed Constants
For configuration that needs structure but not enum behavior:
from dataclasses import dataclass
@dataclass(frozen=True)
class DatabaseConfig:
host: str = "localhost"
port: int = 5432
name: str = "myapp"
pool_size: int = 5
# Immutable, typed, but not an enum
DB_CONFIG = DatabaseConfig()Real-World Patterns
Patterns I use regularly:
API Response Codes
from enum import StrEnum
class ErrorCode(StrEnum):
INVALID_INPUT = "invalid_input"
NOT_FOUND = "not_found"
UNAUTHORIZED = "unauthorized"
RATE_LIMITED = "rate_limited"
INTERNAL_ERROR = "internal_error"
@property
def http_status(self) -> int:
mapping = {
ErrorCode.INVALID_INPUT: 400,
ErrorCode.NOT_FOUND: 404,
ErrorCode.UNAUTHORIZED: 401,
ErrorCode.RATE_LIMITED: 429,
ErrorCode.INTERNAL_ERROR: 500,
}
return mapping[self]
# Usage
def handle_error(code: ErrorCode):
return {"error": code, "status": code.http_status}Database Models with SQLAlchemy
from enum import StrEnum
from sqlalchemy import Column, Enum, String
class OrderStatus(StrEnum):
PENDING = "pending"
PROCESSING = "processing"
SHIPPED = "shipped"
DELIVERED = "delivered"
CANCELLED = "cancelled"
class Order(Base):
__tablename__ = "orders"
id = Column(String, primary_key=True)
status = Column(Enum(OrderStatus), default=OrderStatus.PENDING)CLI Choices with Click
from enum import StrEnum
import click
class OutputFormat(StrEnum):
JSON = "json"
YAML = "yaml"
TABLE = "table"
@click.command()
@click.option(
"--format",
type=click.Choice([f.value for f in OutputFormat]),
default=OutputFormat.TABLE.value
)
def export(format: str):
output_format = OutputFormat(format)
# ...Feature Flags
from enum import Flag, auto
class Feature(Flag):
NONE = 0
DARK_MODE = auto()
BETA_UI = auto()
ADVANCED_SEARCH = auto()
EXPORT_PDF = auto()
# Preset bundles
BASIC = DARK_MODE
PREMIUM = DARK_MODE | ADVANCED_SEARCH | EXPORT_PDF
BETA = DARK_MODE | BETA_UI | ADVANCED_SEARCH | EXPORT_PDF
# User feature check
user_features = Feature.PREMIUM
if Feature.EXPORT_PDF in user_features:
show_export_button()Common Gotchas
Things that tripped me up:
1. Comparing to Values
class Status(Enum):
ACTIVE = "active"
# This is False!
print(Status.ACTIVE == "active") # False
# Use StrEnum or compare .value
print(Status.ACTIVE.value == "active") # True2. Enum Members Are Singletons
# You can't create new instances
status1 = Status("active")
status2 = Status("active")
print(status1 is status2) # True - same object3. Aliases Share Identity
class Status(Enum):
ACTIVE = "active"
ENABLED = "active" # Alias!
# ENABLED is just another name for ACTIVE
print(Status.ENABLED is Status.ACTIVE) # True
# Iteration skips aliases
for s in Status:
print(s.name) # Only prints "ACTIVE"
# Use __members__ to see all names
print(list(Status.__members__.keys())) # ['ACTIVE', 'ENABLED']4. Subclassing Restrictions
class Color(Enum):
RED = 1
GREEN = 2
# Can't subclass an enum that has members!
class ExtendedColor(Color): # TypeError!
BLUE = 3If you need to extend, use a mixin:
class ColorMixin:
def describe(self):
return f"Color: {self.name}"
class Color(ColorMixin, Enum):
RED = 1
GREEN = 2Summary
Enums have become essential in my Python code. Here's my mental model:
- Basic
Enum: Related constants with type safety auto(): When you don't care about the actual valuesIntEnum: When you need integer comparison (database codes, APIs)StrEnum: When you need string comparison (my default for most cases)Flag: When you need to combine options (permissions, features)- Methods: Enums can have behavior, not just values
- Iteration: One of the best features—list all valid values easily
Start using enums wherever you have a closed set of related values. Your IDE will thank you, and typos will become a thing of the past.