Of course! Let's dive into State Machines in Python.

A State Machine (or Finite-State Machine, FSM) is a computational model used to design algorithms and programs that must manage complex logic based on their current "state" and the "events" or "inputs" they receive.
Core Concepts
Before we code, let's understand the key terms:
- State: A condition or situation in the life cycle of the object. For example, a
VendingMachinecan be in anIDLE,WAITING_FOR_COIN, orDISPENSINGstate. - Event/Trigger: An action or occurrence that can cause the state machine to change from one state to another. For example, inserting a coin (
coin_insertedevent) or pressing a selection button (button_pressedevent). - Transition: The movement from one state to another, triggered by an event. For example, from
IDLEtoWAITING_FOR_COINon thecoin_insertedevent. - Action: An activity that is performed in response to a transition. For example, when transitioning from
WAITING_FOR_COINtoDISPENSING, the action might be "dispense the product" and "return change".
Method 1: The "Pure Python" Approach (Using a Dictionary)
This is the most fundamental way to implement a state machine in Python. It's great for understanding the core logic and is perfect for simple to moderately complex machines.
We'll represent the state machine as a dictionary where:

- The keys are the state names.
- The values are another dictionary that maps events to the next state and an optional action.
Example: A Simple Vending Machine
Let's model a vending machine that sells one product for $1.00.
class VendingMachine:
def __init__(self):
# Define all possible states
self.IDLE = 'IDLE'
self.WAITING_FOR_COIN = 'WAITING_FOR_COIN'
self.DISpensING = 'DISPENSING' # Corrected typo: DISPENSING
# The current state of the machine
self.state = self.IDLE
# The cost of the product
self.product_cost = 1.00
self.current_balance = 0.00
def _transition(self, event, *args, **kwargs):
"""
Handles the transition logic based on the current state and event.
This is the core of the state machine.
"""
# Define the state transition table
# Format: {state: {event: (next_state, action_function)}}
transition_table = {
self.IDLE: {
'coin_inserted': (self.WAITING_FOR_COIN, self._add_balance)
},
self.WAITING_FOR_COIN: {
'coin_inserted': (self.WAITING_FOR_COIN, self._add_balance),
'button_pressed': (self.DISPENSING, self._dispense_product)
},
self.DISPENSING: {
'product_dispensed': (self.IDLE, self._reset_balance)
}
}
# Get the valid transitions for the current state
possible_transitions = transition_table.get(self.state, {})
# Check if the event is valid for the current state
if event in possible_transitions:
next_state, action_func = possible_transitions[event]
# Execute the action associated with the transition
if action_func:
action_func(*args, **kwargs)
# Update the current state
self.state = next_state
print(f"Transitioned to new state: {self.state}")
else:
print(f"Error: Event '{event}' is not valid in state '{self.state}'")
# --- Action Methods ---
def _add_balance(self, amount):
"""Action for adding money to the balance."""
self.current_balance += amount
print(f"Added ${amount:.2f}. Current balance: ${self.current_balance:.2f}")
def _dispense_product(self):
"""Action for dispensing the product."""
if self.current_balance >= self.product_cost:
print("Dispensing product...")
self.current_balance -= self.product_cost
else:
print("Error: Not enough balance.")
# Stay in the current state, but we can also define a specific error state
return
def _reset_balance(self):
"""Action for resetting the balance after a transaction."""
self.current_balance = 0.00
print("Balance reset. Transaction complete.")
# --- Public Interface (Events) ---
def insert_coin(self, amount):
"""Public method to trigger the 'coin_inserted' event."""
print(f"\nEvent: insert_coin(${amount:.2f}) in state '{self.state}'")
self._transition('coin_inserted', amount)
def press_button(self):
"""Public method to trigger the 'button_pressed' event."""
print(f"\nEvent: press_button() in state '{self.state}'")
self._transition('button_pressed')
def product_taken(self):
"""Public method to trigger the 'product_dispensed' event."""
print(f"\nEvent: product_taken() in state '{self.state}'")
self._transition('product_dispensed')
# --- Let's run the simulation ---
if __name__ == "__main__":
vm = VendingMachine()
# Scenario 1: Successful purchase
vm.insert_coin(0.50) # State: IDLE -> WAITING_FOR_COIN
vm.insert_coin(0.50) # State: WAITING_FOR_COIN -> WAITING_FOR_COIN
vm.press_button() # State: WAITING_FOR_COIN -> DISPENSING
vm.product_taken() # State: DISPENSING -> IDLE
print("\n" + "="*30 + "\n")
# Scenario 2: Not enough money
vm.insert_coin(0.25) # State: IDLE -> WAITING_FOR_COIN
vm.press_button() # State: WAITING_FOR_COIN -> DISPENSING (fails, but state might stay)
# Let's refine: the action should prevent the transition.
# Our current logic has a flaw. Let's fix it.
# A better way is for the action to return True/False to allow the transition.
# For simplicity, we'll assume pressing the button again after dispensing fails.
# vm.press_button() # This would fail as we are in DISPENSING and 'button_pressed' is not a valid event.
# Let's re-run a corrected scenario
print("\n" + "="*30 + "\n")
vm = VendingMachine() # Reset
vm.insert_coin(1.50) # State: IDLE -> WAITING_FOR_COIN
vm.press_button() # State: WAITING_FOR_COIN -> DISPENSING
print(f"Final balance: ${vm.current_balance:.2f}") # Should be 0.50
vm.product_taken() # State: DISPENSING -> IDLE
print(f"Final balance: ${vm.current_balance:.2f}") # Should be 0.00
Pros of this approach:
- No external libraries needed.
- The logic is centralized in the
transition_table, making it easy to see all possible state changes. - Very explicit and easy to debug.
Cons:
- Can become verbose and hard to manage for very large state machines.
- The logic is not encapsulated within the states themselves.
Method 2: Using a Library (transitions)
For more complex applications, using a dedicated library is highly recommended. The transitions library is a powerful and popular choice. It provides a clean, object-oriented way to define states, transitions, and callbacks.

First, install it:
pip install transitions
Example: The Same Vending Machine with transitions
from transitions import Machine
class VendingMachine:
def __init__(self):
self.product_cost = 1.00
self.current_balance = 0.00
# Define states
states = ['idle', 'waiting_for_coin', 'dispensing']
# Define transitions
transitions = [
{'trigger': 'insert_coin', 'source': 'idle', 'dest': 'waiting_for_coin', 'after': 'add_balance'},
{'trigger': 'insert_coin', 'source': 'waiting_for_coin', 'dest': 'waiting_for_coin', 'after': 'add_balance'},
{'trigger': 'press_button', 'source': 'waiting_for_coin', 'dest': 'dispensing', 'conditions': ['has_sufficient_funds'], 'after': 'dispense_product'},
{'trigger': 'product_taken', 'source': 'dispensing', 'dest': 'idle', 'after': 'reset_balance'},
]
# Initialize the state machine
self.machine = Machine(model=self, states=states, initial='idle', transitions=transitions)
# --- Callbacks (Actions & Conditions) ---
def add_balance(self, amount):
"""Callback for the 'insert_coin' transition."""
# We need to get the amount from the event. The library passes it as a keyword argument.
self.current_balance += amount
print(f"Added ${amount:.2f}. Current balance: ${self.current_balance:.2f}")
def has_sufficient_funds(self):
"""Condition for the 'press_button' transition."""
return self.current_balance >= self.product_cost
def dispense_product(self):
"""Callback for the 'press_button' transition."""
print("Dispensing product...")
self.current_balance -= self.product_cost
print(f"Product dispensed. Remaining balance: ${self.current_balance:.2f}")
def reset_balance(self):
"""Callback for the 'product_taken' transition."""
print("Balance reset. Transaction complete.")
self.current_balance = 0.00
# --- Public Interface (Events) ---
# The library automatically creates these methods from the 'trigger' names
def insert_coin(self, amount):
"""Public method to insert a coin."""
print(f"\nEvent: insert_coin(${amount:.2f}) in state '{self.state}'")
self.insert_coin(amount) # Calls the machine's trigger
def press_button(self):
"""Public method to press the button."""
print(f"\nEvent: press_button() in state '{self.state}'")
self.press_button() # Calls the machine's trigger
def product_taken(self):
"""Public method to take the product."""
print(f"\nEvent: product_taken() in state '{self.state}'")
self.product_taken() # Calls the machine's trigger
# --- Let's run the simulation ---
if __name__ == "__main__":
vm = VendingMachine()
# Scenario 1: Successful purchase
vm.insert_coin(0.50)
vm.insert_coin(0.50)
vm.press_button()
vm.product_taken()
print("\n" + "="*30 + "\n")
# Scenario 2: Not enough money
vm.insert_coin(0.25)
vm.press_button() # This transition will NOT happen because the condition 'has_sufficient_funds' is False.
print(f"Current state is still: {vm.state}") # Will still be 'waiting_for_coin'
vm.insert_coin(0.75)
vm.press_button() # Now the condition is met
vm.product_taken()
Pros of using transitions:
- Cleaner Code: States and transitions are defined declaratively.
- Powerful Features: Supports nested states, state callbacks (
on_enter,on_exit), conditions, and visual graphing. - Less Boilerplate: The library handles the dispatching logic for you.
- Scalability: Much easier to manage for large and complex state machines.
Cons:
- Adds an external dependency to your project.
Which Approach Should You Choose?
-
Use the Pure Python approach if:
- Your state machine is very simple.
- You want to avoid external dependencies.
- You are learning the fundamentals of state machines.
-
Use the
transitionslibrary if:- Your state machine has more than a few states and transitions.
- You need features like conditions or callbacks.
- You want to visualize your state machine (
vm.machine.get_graph().draw('my_state_machine.png', prog='dot')). - You want to write cleaner, more maintainable code. This is the recommended approach for most real-world applications.
