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 = 3

Then 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:

  1. No grouping: Related constants are just variables floating around
  2. No validation: Nothing stops you from using "compelted" (typo)
  3. No IDE help: Autocomplete shows every constant, not just valid statuses
  4. 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 Status

This 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) # 4

auto() 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")  # True

Flag: 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) # False

Flag 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.WRITE

I 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.8

Dict-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) # False

Methods 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))  # True

Class 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.HIGH

Enum 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:

  1. Values are related and form a closed set
  2. Type safety matters—you want to prevent invalid values
  3. You need iteration over all valid values
  4. IDE support is valuable (autocomplete, type hints)
  5. Values have behavior (methods, computed properties)
# Good enum use case: task statuses
class TaskStatus(Enum):
    PENDING = "pending"
    COMPLETED = "completed"

Use Constants Module When:

  1. Values are independent configuration settings
  2. Values change between environments (dev/prod)
  3. Values are used for external systems (API keys, URLs)
  4. 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 = 50

The 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")  # True

2. Enum Members Are Singletons

# You can't create new instances
status1 = Status("active")
status2 = Status("active")
print(status1 is status2)  # True - same object

3. 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 = 3

If you need to extend, use a mixin:

class ColorMixin:
    def describe(self):
        return f"Color: {self.name}"
 
class Color(ColorMixin, Enum):
    RED = 1
    GREEN = 2

Summary

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 values
  • IntEnum: 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.

React to this post: