Enum Methods and Customization¶
Add methods to enums and customize their behavior to create powerful, expressive types.
Mental Model
An enum is not just a bag of constants -- it is a class, and its members are instances. This means you can add methods, properties, and even __init__ to give each member domain-specific behavior. Instead of scattering if status == ... checks across your codebase, put the logic on the enum itself: status.next_state().
Adding Methods to Enums¶
```python from enum import Enum
class Size(Enum): SMALL = 1 MEDIUM = 2 LARGE = 3 XLARGE = 4
def get_next_size(self):
'''Get the next larger size'''
members = list(Size)
idx = members.index(self)
if idx < len(members) - 1:
return members[idx + 1]
return self
def get_price_multiplier(self):
'''Get price multiplier for this size'''
multipliers = {
Size.SMALL: 1.0,
Size.MEDIUM: 1.25,
Size.LARGE: 1.5,
Size.XLARGE: 1.75
}
return multipliers[self]
small = Size.SMALL print(small.get_next_size()) # Size.MEDIUM print(small.get_price_multiplier()) # 1.0 ```
Closed Polymorphism
Enums with methods are a form of closed polymorphism — the set of variants is fixed at definition time, and each member can carry its own behavior. This is similar to algebraic data types in functional languages. Unlike open polymorphism (class inheritance, where anyone can add new subclasses), enum polymorphism is exhaustive — you can handle every case and be certain you haven't missed one. This makes enums ideal for state machines, command patterns, and any domain where the set of options is known and complete.
Properties in Enums¶
```python from enum import Enum from functools import cached_property
class UserRole(Enum): GUEST = "guest" USER = "user" MODERATOR = "moderator" ADMIN = "admin"
@property
def can_delete_posts(self):
'''Check if role can delete posts'''
return self in (UserRole.MODERATOR, UserRole.ADMIN)
@property
def can_ban_users(self):
'''Check if role can ban users'''
return self == UserRole.ADMIN
@property
def display_name(self):
'''Human-readable role name'''
return self.value.capitalize()
role = UserRole.MODERATOR print(f"Role: {role.display_name}") # Role: Moderator print(f"Can delete posts: {role.can_delete_posts}") # True print(f"Can ban users: {role.can_ban_users}") # False ```
Class Methods in Enums¶
```python from enum import Enum
class Environment(Enum): DEVELOPMENT = "development" STAGING = "staging" PRODUCTION = "production"
@classmethod
def from_string(cls, value: str):
'''Create from string, case-insensitive'''
for member in cls:
if member.value.lower() == value.lower():
return member
raise ValueError(f"Unknown environment: {value}")
@classmethod
def is_production(cls, env):
'''Check if environment is production'''
return env == cls.PRODUCTION
env = Environment.from_string("PRODUCTION") print(env) # Environment.PRODUCTION print(Environment.is_production(env)) # True ```
Custom Enum with str¶
```python from enum import Enum
class Status(Enum): PENDING = "pending" RUNNING = "running" COMPLETED = "completed" FAILED = "failed"
def __str__(self):
'''Custom string representation'''
symbols = {
Status.PENDING: "⏳",
Status.RUNNING: "▶️",
Status.COMPLETED: "✅",
Status.FAILED: "❌"
}
return f"{symbols[self]} {self.value}"
status = Status.RUNNING print(status) # ▶️ running print(str(status)) # ▶️ running ```
Enum with repr¶
```python from enum import Enum
class Priority(Enum): LOW = 1 MEDIUM = 2 HIGH = 3 CRITICAL = 4
def __repr__(self):
'''Detailed representation'''
return f"<Priority: {self.name} (level {self.value})>"
priority = Priority.HIGH
print(repr(priority)) #
Computed Enum Values¶
```python from enum import Enum from datetime import datetime
class LogLevel(Enum): DEBUG = (1, "DEBUG", "🔍") INFO = (2, "INFO", "ℹ️") WARNING = (3, "WARNING", "⚠️") ERROR = (4, "ERROR", "❌") CRITICAL = (5, "CRITICAL", "🔴")
def __init__(self, level, name_str, symbol):
self.level = level
self.name_str = name_str
self.symbol = symbol
def format_message(self, message: str) -> str:
'''Format log message'''
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
return f"[{timestamp}] {self.symbol} {self.name_str}: {message}"
log = LogLevel.ERROR print(log.format_message("Database connection failed")) ```
Customizing Lookup¶
```python from enum import Enum
class ErrorCode(Enum): NOT_FOUND = 404 FORBIDDEN = 403 SERVER_ERROR = 500
def __init__(self, code):
self.code = code
@classmethod
def from_http_status(cls, status_code: int):
'''Look up by HTTP status code'''
for member in cls:
if member.code == status_code:
return member
raise ValueError(f"No enum for status code: {status_code}")
def get_message(self) -> str:
'''Get friendly error message'''
messages = {
404: "Resource not found",
403: "Access forbidden",
500: "Internal server error"
}
return messages.get(self.code, "Unknown error")
error = ErrorCode.from_http_status(404) print(f"{error.code}: {error.get_message()}") # 404: Resource not found ```
When to Add Methods¶
- Validation logic
- Conversion operations
- Related computations
- Display formatting
- Lookup functionality
When NOT to Add Methods
Enums with methods are powerful, but not every enum needs them. Avoid adding methods when:
- The logic doesn't depend on the enum member itself — use a standalone function instead.
- The method grows complex enough to warrant its own class — this is a sign you need a full class hierarchy, not a method-heavy enum.
- The enum is used purely as a set of constants (e.g., configuration keys) — methods add complexity without value.
A good rule of thumb: if your enum has more method lines than member definitions, consider whether a class with an enum attribute would be clearer.
Exercises¶
Exercise 1.
Create a Season enum with a method is_warm() that returns True for SUMMER and SPRING. Add a __str__ override that returns a formatted string like "Season: Spring". Iterate over all seasons and print which are warm.
Solution to Exercise 1
from enum import Enum
class Season(Enum):
SPRING = 1
SUMMER = 2
AUTUMN = 3
WINTER = 4
def is_warm(self):
return self in (Season.SPRING, Season.SUMMER)
def __str__(self):
return f"Season: {self.name.capitalize()}"
for s in Season:
warm = "warm" if s.is_warm() else "cold"
print(f"{s} ({warm})")
Exercise 2.
Define a Coin enum with PENNY = 1, NICKEL = 5, DIME = 10, QUARTER = 25. Add a dollar_value property that returns the value in dollars (e.g., 0.25 for QUARTER). Add a class method total(coins) that returns the total dollar value of a list of coins.
Solution to Exercise 2
from enum import Enum
class Coin(Enum):
PENNY = 1
NICKEL = 5
DIME = 10
QUARTER = 25
@property
def dollar_value(self):
return self.value / 100
@classmethod
def total(cls, coins):
return sum(c.dollar_value for c in coins)
coins = [Coin.QUARTER, Coin.DIME, Coin.NICKEL, Coin.PENNY]
print(f"Total: ${Coin.total(coins):.2f}") # Total: $0.41
Exercise 3.
Create a Direction enum with a opposite property that returns the opposite direction (NORTH returns SOUTH, etc.). Also add a rotate(steps) method that rotates clockwise by the given number of 90-degree steps. Demonstrate both features.
Solution to Exercise 3
from enum import Enum
class Direction(Enum):
NORTH = 0
EAST = 1
SOUTH = 2
WEST = 3
@property
def opposite(self):
return Direction((self.value + 2) % 4)
def rotate(self, steps=1):
return Direction((self.value + steps) % 4)
print(Direction.NORTH.opposite) # Direction.SOUTH
print(Direction.EAST.opposite) # Direction.WEST
print(Direction.NORTH.rotate(1)) # Direction.EAST
print(Direction.NORTH.rotate(3)) # Direction.WEST
Exercise 4.
An enum has grown to include a complex process() method with 20+ lines of logic. A colleague argues this is fine because "enums can have methods." Explain why this is a design smell and propose a refactored design that keeps the enum simple. Illustrate with a before/after sketch.
Solution to Exercise 4
Why it's a smell: Enums are meant to be lightweight symbolic constants. When methods grow complex, the enum takes on responsibilities that belong to a separate class. This makes the enum harder to test, harder to extend, and harder to read.
Before (design smell):
class TaskStatus(Enum):
PENDING = "pending"
RUNNING = "running"
DONE = "done"
def process(self, task, db, logger, notifier):
if self == TaskStatus.PENDING:
# 20 lines of scheduling logic...
pass
elif self == TaskStatus.RUNNING:
# 20 lines of monitoring logic...
pass
elif self == TaskStatus.DONE:
# 20 lines of cleanup logic...
pass
After (clean separation):
class TaskStatus(Enum):
PENDING = "pending"
RUNNING = "running"
DONE = "done"
class TaskProcessor:
def process(self, status: TaskStatus, task):
handler = {
TaskStatus.PENDING: self._schedule,
TaskStatus.RUNNING: self._monitor,
TaskStatus.DONE: self._cleanup,
}
return handler[status](task)
Rule of thumb: if a method needs dependencies beyond self (databases, loggers, external services), it belongs in a service class, not the enum.
Exercise 5.
Create a Temperature enum with members FREEZING = 0, COLD = 10, WARM = 20, HOT = 30 (values in Celsius). Add a to_fahrenheit property, a __str__ that shows both units, and a class method from_celsius(temp) that returns the closest matching member. Demonstrate all three.
Solution to Exercise 5
from enum import Enum
class Temperature(Enum):
FREEZING = 0
COLD = 10
WARM = 20
HOT = 30
@property
def to_fahrenheit(self):
return self.value * 9 / 5 + 32
def __str__(self):
return f"{self.name}: {self.value}°C / {self.to_fahrenheit:.0f}°F"
@classmethod
def from_celsius(cls, temp):
return min(cls, key=lambda m: abs(m.value - temp))
# Property
print(Temperature.FREEZING.to_fahrenheit) # 32.0
# __str__
print(Temperature.HOT) # HOT: 30°C / 86°F
# Class method
print(Temperature.from_celsius(15)) # Temperature.COLD (closest to 15)
print(Temperature.from_celsius(28)) # Temperature.HOT (closest to 28)