Arjan Codes replaces inheritance with decorators for cleaner state machines

ArjanCodes////4 min read

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.

Arjan Codes replaces inheritance with decorators for cleaner state machines
The State Pattern Done the Pythonic Way

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:

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.

    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.

Topic DensityMention share of the most discussed topics · 10 mentions across 10 distinct topics
Data-driven Design
10%· programming
Decorator
10%· programming
Enums
10%· programming
Generics
10%· programming
Open-Closed Principle
10%· programming
Other topics
50%
End of Article
Source video
Arjan Codes replaces inheritance with decorators for cleaner state machines

The State Pattern Done the Pythonic Way

Watch

ArjanCodes // 26:23

On this channel, I post videos about programming and software design to help you take your coding skills to the next level. I'm an entrepreneur and a university lecturer in computer science, with more than 20 years of experience in software development and design. If you're a software developer and you want to improve your development skills, and learn more about programming in general, make sure to subscribe for helpful videos. I post a video here every Friday. If you have any suggestion for a topic you'd like me to cover, just leave a comment on any of my videos and I'll take it under consideration. Thanks for watching!

What they talk about
AI and Agentic Coding News
Who and what they mention most
Python
25.0%3
Python
25.0%3
Python
16.7%2
4 min read0%
4 min read