Replace Boolean Soup with Python Enum and Match/Case
The Problem
I had a function with way too many boolean flags. It looked like this:
def process_order(order): if order.is_pending and not order.is_paid and not order.is_shipped: # Handle pending order pass elif order.is_paid and not order.is_shipped and not order.is_cancelled: # Handle paid order waiting for shipment pass elif order.is_shipped and not order.is_delivered: # Handle shipped order pass elif order.is_cancelled: # Handle cancelled order pass elif order.is_delivered: # Handle delivered order pass else: # What state is this??? passI kept asking myself: What happens when both is_pending and is_paid are true? Is that even valid? What about is_cancelled and is_shipped together?
This is “boolean soup” - a mess of overlapping boolean flags that should be mutually exclusive states. The code is hard to read, hard to debug, and very easy to break.
The Solution: Enum with Match/Case
I refactored this to use Python’s enum.Enum combined with match/case (available in Python 3.10+). Here’s what I ended up with:
from enum import Enum, auto
class OrderStatus(Enum): PENDING = auto() PAID = auto() SHIPPED = auto() DELIVERED = auto() CANCELLED = auto()The auto() function automatically assigns integer values (1, 2, 3, …), so I don’t have to manually set them. The state names are self-documenting.
Now my processing function looks like this:
def process_order(order): match order.status: case OrderStatus.PENDING: handle_pending(order) case OrderStatus.PAID: handle_paid(order) case OrderStatus.SHIPPED: handle_shipped(order) case OrderStatus.DELIVERED: handle_delivered(order) case OrderStatus.CANCELLED: handle_cancelled(order)This is much clearer. Each state has exactly one handler, and there’s no ambiguity about overlapping conditions.
Before and After Comparison
Let me show you the full refactoring. Here’s the “before” version with boolean soup:
class Order: def __init__(self): self.is_pending = True self.is_paid = False self.is_shipped = False self.is_delivered = False self.is_cancelled = False
def can_transition_to_shipped(self): # Complex logic to check if we can ship return (self.is_paid and not self.is_shipped and not self.is_delivered and not self.is_cancelled)
def can_transition_to_cancelled(self): # Another complex check return (self.is_pending or self.is_paid) and not self.is_shipped
def transition_to_shipped(self): if self.can_transition_to_shipped(): self.is_shipped = True # What about is_pending? is_paid? Keep them all? # This is confusing! return True return FalseI had to track multiple boolean flags, and I was never sure which ones should be set or cleared during transitions.
Here’s the “after” version with Enum:
from enum import Enum, auto
class OrderStatus(Enum): PENDING = auto() PAID = auto() SHIPPED = auto() DELIVERED = auto() CANCELLED = auto()
class Order: # Define valid transitions VALID_TRANSITIONS = { OrderStatus.PENDING: {OrderStatus.PAID, OrderStatus.CANCELLED}, OrderStatus.PAID: {OrderStatus.SHIPPED, OrderStatus.CANCELLED}, OrderStatus.SHIPPED: {OrderStatus.DELIVERED}, OrderStatus.DELIVERED: set(), # Terminal state OrderStatus.CANCELLED: set(), # Terminal state }
def __init__(self): self.status = OrderStatus.PENDING
def can_transition_to(self, new_status): return new_status in self.VALID_TRANSITIONS[self.status]
def transition_to(self, new_status): if self.can_transition_to(new_status): self.status = new_status return True raise ValueError( f"Cannot transition from {self.status} to {new_status}" )The state transitions are now explicit and self-documenting. I can see at a glance which states can transition to which other states.
State Transition Diagram
With Enum, I can visualize the valid state flow:
PENDING ──┬──> PAID ──┬──> SHIPPED ──> DELIVERED │ │ │ └──> CANCELLED │ └──> CANCELLEDThis diagram is derived directly from the VALID_TRANSITIONS dictionary. It’s impossible to transition from DELIVERED to anything else - that’s a terminal state.
Type Safety with mypy and pyright
One big advantage of using Enum is type checking. With boolean flags, mypy can’t help me:
# Boolean soup - no type checking possibleorder.is_pending = Trueorder.is_paid = True # Is this valid? mypy doesn't knoworder.is_shipped = True # Can both be true? No warningWith Enum, I get type safety:
# Type-safe - mypy/pyright will catch errorsorder.status = OrderStatus.PENDING # OKorder.status = OrderStatus.PAID # OKorder.status = "paid" # Type error! mypy/pyright will complainorder.status = 1 # Type error!When I run mypy:
$ mypy order_after.pyorder_after.py:25: error: Incompatible types in assignment(expression has type "str", variable has type "OrderStatus")This catches bugs at development time instead of runtime.
Match/Case Pattern Matching
Python 3.10 introduced structural pattern matching with match/case. Combined with Enum, it makes state handling clean:
def get_order_message(order): match order.status: case OrderStatus.PENDING: return "Your order is being processed" case OrderStatus.PAID: return "Payment received, preparing for shipment" case OrderStatus.SHIPPED: return f"Your order is on the way! Tracking: {order.tracking_number}" case OrderStatus.DELIVERED: return "Your order has been delivered" case OrderStatus.CANCELLED: return "This order has been cancelled"I can also use guard clauses for more complex conditions:
def calculate_refund(order): match order.status: case OrderStatus.PENDING: return order.total # Full refund case OrderStatus.PAID if order.days_since_payment < 30: return order.total * 0.9 # 10% restocking fee case OrderStatus.PAID: return order.total * 0.7 # 30% restocking fee case OrderStatus.SHIPPED | OrderStatus.DELIVERED: return 0 # No refund after shipping case OrderStatus.CANCELLED: return 0Handling Unknown States
One pattern I use is handling unexpected states gracefully:
def process_order_safe(order): match order.status: case OrderStatus.PENDING: handle_pending(order) case OrderStatus.PAID: handle_paid(order) case OrderStatus.SHIPPED: handle_shipped(order) case OrderStatus.DELIVERED: handle_delivered(order) case OrderStatus.CANCELLED: handle_cancelled(order) case _: # This catches any unknown states raise RuntimeError(f"Unknown order status: {order.status}")The case _: is like the else clause - it catches anything that doesn’t match the previous cases.
When to Use Enum vs Boolean
I use Enum when:
- States are mutually exclusive (an order can’t be both pending and shipped)
- I have more than 2 states (booleans work for exactly 2 states)
- I want type checking
- I want to document all valid states explicitly
I still use booleans when:
- There are only 2 states (is_valid, is_empty)
- The flags are truly independent (is_vip and is_first_time_buyer can both be true)
Here’s an example where booleans make sense:
class Customer: def __init__(self): # These are independent flags - both can be true self.is_vip = False self.is_first_time_buyer = True
def get_discount(self): discount = 0 if self.is_vip: discount += 10 if self.is_first_time_buyer: discount += 5 return discountThese booleans represent independent dimensions, not mutually exclusive states. Converting them to Enum would be over-engineering.
Complete Example: Order Processing
Let me put it all together with a complete order processing example:
from enum import Enum, autofrom datetime import datetime
class OrderStatus(Enum): PENDING = auto() PAID = auto() SHIPPED = auto() DELIVERED = auto() CANCELLED = auto()
class Order: VALID_TRANSITIONS = { OrderStatus.PENDING: {OrderStatus.PAID, OrderStatus.CANCELLED}, OrderStatus.PAID: {OrderStatus.SHIPPED, OrderStatus.CANCELLED}, OrderStatus.SHIPPED: {OrderStatus.DELIVERED}, OrderStatus.DELIVERED: set(), OrderStatus.CANCELLED: set(), }
def __init__(self, order_id: str, total: float): self.order_id = order_id self.total = total self.status = OrderStatus.PENDING self.tracking_number: str | None = None self.created_at = datetime.now()
def transition_to(self, new_status: OrderStatus) -> None: if not self.can_transition_to(new_status): raise ValueError( f"Invalid transition: {self.status} -> {new_status}" ) self.status = new_status
def can_transition_to(self, new_status: OrderStatus) -> bool: return new_status in self.VALID_TRANSITIONS[self.status]
def pay(self) -> None: self.transition_to(OrderStatus.PAID)
def ship(self, tracking_number: str) -> None: self.tracking_number = tracking_number self.transition_to(OrderStatus.SHIPPED)
def deliver(self) -> None: self.transition_to(OrderStatus.DELIVERED)
def cancel(self) -> None: self.transition_to(OrderStatus.CANCELLED)
def __str__(self) -> str: return f"Order({self.order_id}, {self.status.name}, ${self.total})"Using the Order class:
# Create an orderorder = Order("ORD-123", 99.99)print(order) # Order(ORD-123, PENDING, $99.99)
# Process the orderorder.pay()print(order) # Order(ORD-123, PAID, $99.99)
order.ship("TRACK-ABC-123")print(order) # Order(ORD-123, SHIPPED, $99.99)
order.deliver()print(order) # Order(ORD-123, DELIVERED, $99.99)
# This will raise an error - can't cancel a delivered ordertry: order.cancel()except ValueError as e: print(e) # Invalid transition: DELIVERED -> CANCELLEDSummary
I refactored from boolean soup to Enum with match/case, and the benefits were immediate:
- Clarity - State names are self-documenting (
OrderStatus.PENDINGvsis_pending=True) - Type safety - mypy and pyright catch invalid state assignments
- Explicit transitions - The valid state transitions are documented in code
- Clean pattern matching -
match/casereads better than nestedif/elif - Single source of truth - One
statusfield instead of multiple booleans
The key insight is that when you have mutually exclusive states, Enum is the right tool. When you have independent flags that can coexist, booleans still make sense.
For Python 3.10+, combining enum.Enum with match/case gives you clean, type-safe state management that’s easy to read and hard to break.
Final Words + More Resources
My intention with this article was to help others share my knowledge and experience. If you want to contact me, you can contact by email: Email me
Here are also the most important links from this article along with some further resources that will help you in this scope:
- 👨💻 Python Enum Documentation
- 👨💻 PEP 634 - Structural Pattern Matching
- 👨💻 Python 3.10 Release Notes
Oh, and if you found these resources useful, don’t forget to support me by starring the repo on GitHub!
Comments