The traditional state pattern problem Classic State Pattern implementations often descend into a nightmare of boilerplate and deep inheritance trees. In the traditional approach, you define a common protocol or base class for all possible states and then implement concrete classes for each state—such as `New`, `Authorized`, or `Refunded`. While this avoids massive `if-else` blocks, it forces you to duplicate error-handling logic across multiple classes for invalid transitions. If a `Refunded` state shouldn't allow a `Capture` event, you must manually raise a `RuntimeError` in that specific class. This scatters the logic of your state machine across dozen of files, making it nearly impossible to visualize the entire flow at a glance. Reframing states as a lookup table At its core, a state machine is just a lookup table. It maps a current state and a specific event to a resulting state and an action. Instead of hiding this data structure inside Polymorphism, we can make it explicit. By treating state transitions as data, we can build a generic engine that handles the mechanics of state management, leaving our business logic clean and isolated. This shift in perspective transforms the state pattern from an inheritance-heavy structure into a Data-driven Design problem, which is where Python truly shines. Building a generic state machine engine To build a reusable engine, we utilize Generics to define the types for states, events, and context. The engine relies on a `transitions` dictionary where the key is a tuple of `(state, event)` and the value is a tuple of `(next_state, action_function)`. Here is how you can structure the core `StateMachine` class: ```python from dataclasses import dataclass, field from typing import Generic, TypeVar, Callable, Enum S = TypeVar("S", bound=Enum) E = TypeVar("E", bound=Enum) C = TypeVar("C") @dataclass class StateMachine(Generic[S, E, C]): transitions: dict[tuple[S, E], tuple[S, Callable[[C], None]]] = field(default_factory=dict) def add_transition(self, from_state: S, event: E, to_state: S, action: Callable[[C], None]): self.transitions[(from_state, event)] = (to_state, action) def handle(self, context: C, current_state: S, event: E) -> S: if (current_state, event) not in self.transitions: raise ValueError(f"Invalid transition from {current_state} via {event}") next_state, action = self.transitions[(current_state, event)] action(context) return next_state ``` Implementing declarative transitions with decorators Manually calling `add_transition` for every possible flow is tedious and error-prone. We can improve this using a Decorator. Decorators allow us to define transitions directly above the action functions they trigger. This makes the code almost read like a formal specification. By expanding the decorator to accept an `Iterable` of states, we can handle multiple "from" states—like `New` or `Authorized` failing—without duplicating the action logic. ```python def transition(self, from_states: S | iterable[S], event: E, to_state: S): def decorator(action: Callable[[C], None]): states = [from_states] if isinstance(from_states, Enum) else from_states for state in states: self.add_transition(state, event, to_state, action) return action return decorator ``` Why boring classes are better By moving the transition logic into the StateMachine engine, the primary business class (like `Payment`) becomes "boring." It no longer needs to know about state transition rules; it only needs to store the current state and context. This separation is crucial for scalability. You can use the same `StateMachine` instance across thousands of different `Payment` objects because the state is stored in the object, not the engine. This keeps the engine stateless and the business objects lightweight, fulfilling the Open-Closed Principle where you can add new states and transitions just by adding new decorated functions. Syntax and practical application This approach relies on Python’s first-class functions and enums. Using Enums ensures type safety and prevents magic strings from creeping into your logic. This pattern is ideal for complex workflows like Payment Gateways, order processing, or logistics systems where illegal transitions must be strictly blocked. While the classic class-based approach might still work for states with massive internal data, this decorator-driven engine is almost always the superior choice for modern Python development.
Data-driven Design
Programming
- Apr 10, 2026