DEV Community

Kaushikcoderpy
Kaushikcoderpy

Posted on • Originally published at logicandlegacy.blogspot.com

Python State Machines: FSMs, The State Pattern & Transitions (2026)

Day 25: The State Machine — Eliminating Boolean Blindness

11 min read
Series: Logic & Legacy
Day 25 / 30
Level: Senior Architecture

Context: We have secured the network and built fault tolerance into our exceptions. But there is a silent killer in every codebase that no try/except block can catch. It is the bug of "Invalid State." Today, we cure it.

The Disease: Boolean Blindness

A technical infographic contrasting two software development approaches for managing complex object states (using an e-commerce order example). The left section, titled

Look at the database schema of any Junior Developer's e-commerce application. You will inevitably find an Order table that looks like this:

  • is_draft = Boolean
  • is_paid = Boolean
  • is_shipped = Boolean
  • is_refunded = Boolean
  • is_cancelled = Boolean

Because there are 5 boolean columns, mathematically, this system can exist in 2⁵ (32) different states. What happens when a glitch in the code sets is_shipped = True AND is_cancelled = True?

The system collapses into a logical paradox. The developer tries to fix it by writing massive, unreadable if/elif/else statements (if is_shipped and not is_cancelled and is_paid: ...). The code becomes a fragile spiderweb. This is called Boolean Blindness.

▶ Table of Contents 🕉️ (Click to Expand)

  1. The Cure: Finite State Machines (FSM)
  2. The Architecture of a State Machine
  3. Implementing the OOP State Pattern
  4. Production Reality: Declarative Transitions > "A system that can be in two contradictory states at once is not a system. It is a ticking time bomb."

1. The Cure: Finite State Machines (FSM)

A Finite State Machine (FSM) is a computational mathematical model. It enforces one absolute, unbreakable law:

The machine can only be in exactly ONE state at any given time.

Instead of 5 boolean flags, you have a single field: state. An Order is either DRAFT, PAID, SHIPPED, or CANCELLED. It is physically impossible to be both Shipped and Cancelled simultaneously.

2. The Architecture of a State Machine

A State Machine consists of three structural pillars:

  • 1. States (Nodes): The distinct statuses an object can inhabit (e.g., Solid, Liquid, Gas).
  • 2. Events (Triggers): The action attempting to change the state (e.g., Apply Heat, Apply Cold).
  • 3. Transitions (Edges): The strict rules dictating if an Event is allowed to move the object from State A to State B. (e.g., You can transition from Solid to Liquid, but you cannot transition from Solid directly to Gas without sublimation logic).

3. Implementing the OOP State Pattern

How do we build this in Python? The amateur way is to write a massive match/case or if/else block inside the object. As your application grows to 20 states, this function becomes 500 lines long.

Senior Architects use the State Design Pattern (from the Gang of Four). We rely on Polymorphism and Abstract Base Classes (ABCs). Instead of the Order class managing all the messy logic, we create a distinct class for every state. The Order simply delegates its behavior to whatever state class it currently holds.

The OOP State Pattern Architecture

from abc import ABC, abstractmethod

# 1. The Abstract Base Class (The Blueprint)
class OrderState(ABC):
    @abstractmethod
    def pay(self, order): pass

    @abstractmethod
    def ship(self, order): pass

# 2. Concrete State Classes
class DraftState(OrderState):
    def pay(self, order):
        print("Payment successful. Transitioning to PAID.")
        # The State object mutates the Order's state!
        order.set_state(PaidState()) 

    def ship(self, order):
        # Rejection rule: You cannot ship a draft.
        raise RuntimeError("Cannot ship an unpaid order!")

class PaidState(OrderState):
    def pay(self, order):
        raise RuntimeError("Order is already paid.")

    def ship(self, order):
        print("Box dispatched. Transitioning to SHIPPED.")
        order.set_state(ShippedState())

class ShippedState(OrderState):
    # Terminal state. It can do neither.
    def pay(self, order): raise RuntimeError("Already shipped.")
    def ship(self, order): raise RuntimeError("Already shipped.")

# 3. The Context (The object the user actually interacts with)
class Order:
    def __init__(self):
        # Initial State
        self._state = DraftState()

    def set_state(self, state: OrderState):
        self._state = state

    # Delegation! The Order doesn't have 'if' logic. It just asks the State.
    def pay(self): self._state.pay(self)
    def ship(self): self._state.ship(self)

# Execution:
my_order = Order()
my_order.pay()  # Works! Transitions to Paid.
my_order.ship() # Works! Transitions to Shipped.
# my_order.pay() # Would raise an Error. State is safely locked.
Enter fullscreen mode Exit fullscreen mode

Why is this powerful? If Product Management asks you to add a "Refunded" state, you don't have to touch 500 lines of existing if/else logic. You simply create a RefundedState class. This satisfies the Open-Closed Principle (Open for extension, closed for modification).

4. Production Reality: Declarative Transitions

While the OOP pattern above is mathematically perfect and excellent for learning, writing a separate class for 15 different states is verbose.

In production, Architects use Declarative FSM Libraries (like the popular Python transitions package or django-fsm). These libraries allow you to define the states and edges dynamically using dictionaries, and they automatically generate the methods for you.

The Declarative Approach (Transitions Library)

# pip install transitions
from transitions import Machine

class Article:
    pass

article = Article()

# Define the nodes
states = ['draft', 'review', 'published', 'archived']

# Define the edges (trigger, source, destination)
transitions = [
    {'trigger': 'submit', 'source': 'draft', 'dest': 'review'},
    {'trigger': 'approve', 'source': 'review', 'dest': 'published'},
    {'trigger': 'archive', 'source': '*', 'dest': 'archived'} # '*' means any state
]

# Bind the machine to our empty class
machine = Machine(model=article, states=states, transitions=transitions, initial='draft')

# The library magically created these methods based on our triggers!
article.submit()
print(article.state) # Outputs: 'review'

# article.archive() # Would transition to 'archived' from anywhere.
Enter fullscreen mode Exit fullscreen mode

🛠️ Day 25 Project: The Document Flow

Build an unbreakable State Machine from scratch.

  • Replicate the OOP State Pattern for a Document object.
  • It must have three states: Draft, InReview, and Published.
  • The Document should have an approve() method and a reject() method. Rejection should send it backward to Draft. Approval should send it forward.

🔥 PRO UPGRADE (The "Guard" Clause)

In advanced State Machines, a transition isn't just approved based on the current state; it also requires a mathematical check. Your challenge: Modify your DraftState so that the submit() method accepts a word_count argument. Implement a Guard Clause: the document can only transition to InReview IF the state is currently Draft AND the word_count > 500. Otherwise, it raises an exception.

5. FAQ: State Architecture

Is an FSM the same as a DAG (Directed Acyclic Graph)?

No. A DAG (like what Airflow or Celery uses for tasks) has a distinct start and finish, and data flows in one direction. It is "Acyclic" meaning no cycles (loops) are allowed. A Finite State Machine allows cycles. A user can transition from LoggedOut -> LoggedIn -> LoggedOut infinitely.

How do State Machines work with databases (SQLAlchemy/Django)?

In the database, the state is still saved as a simple String column (e.g., status VARCHAR(20)). The FSM library wraps your ORM Model. When you call order.ship(), the Python library verifies the transition rules in memory, and if allowed, it updates the string to "SHIPPED" and issues an UPDATE SQL command to the database.

📚 Flow Resources

State: Locked

You have eliminated Boolean Blindness and brought mathematical order to your system's lifecycle. Hit Follow to catch Day 26.

[← Previous

Day 24: Exceptions & Fault Tolerance](https://logicandlegacy.blogspot.com/2026/03/day-24-exceptions.html)
[Next →

Day 26: The Configuration Layer — Environment Control](#)


Originally published at https://logicandlegacy.blogspot.com

Top comments (0)