Python State Machines with Transitions — Core Concepts
What the Transitions Library Does
transitions is a lightweight Python library that adds state machine behavior to any object. You define states, transitions between them, and optional conditions and callbacks. The library then:
- Tracks the current state
- Only allows valid transitions
- Runs callbacks before, during, and after transitions
- Provides convenience methods for checking state (
is_<state>()) - Generates trigger methods automatically (
<trigger_name>())
Basic Setup
A state machine needs three things: states, transitions, and a model (the object whose state is being tracked).
states = ["draft", "review", "approved", "published"]
transitions = [
{"trigger": "submit", "source": "draft", "dest": "review"},
{"trigger": "approve", "source": "review", "dest": "approved"},
{"trigger": "reject", "source": "review", "dest": "draft"},
{"trigger": "publish", "source": "approved", "dest": "published"},
]
Each transition has:
- trigger — the method name that causes the transition
- source — the state you must be in to use this trigger
- dest — the state you move to
Callbacks: Running Code During Transitions
Transitions supports callbacks at multiple points in the transition lifecycle:
| Callback | When it runs | Use case |
|---|---|---|
before | Before the transition starts | Validation, logging |
prepare | Before conditions are checked | Data preparation |
conditions | After prepare, before transition | Guard clauses |
after | After the transition completes | Side effects (emails, logs) |
on_enter_<state> | When entering a specific state | State-specific setup |
on_exit_<state> | When leaving a specific state | State-specific cleanup |
Example: sending an email when an article is published:
after publish → send_notification_email
on_enter_published → update_publish_timestamp
Conditions (Guards)
Conditions are functions that return True or False. A transition only proceeds if all conditions pass:
{"trigger": "approve", "source": "review", "dest": "approved",
"conditions": ["has_reviewer_approval", "passes_quality_check"]}
If has_reviewer_approval returns False, calling approve() does nothing (or raises an exception, depending on configuration).
Multiple Sources and Wildcards
A trigger can apply to multiple source states:
{"trigger": "cancel", "source": ["draft", "review", "approved"], "dest": "cancelled"}
Or use the wildcard "*" to allow a transition from any state:
{"trigger": "emergency_stop", "source": "*", "dest": "halted"}
State Features
States aren’t just strings. They can carry behavior:
on_entercallbacks — run when entering the stateon_exitcallbacks — run when leaving the stateignore_invalid_triggers— silently ignore triggers that don’t apply instead of raising errors- Tags — group states for bulk operations (e.g., all “active” states)
Querying State
The library adds convenience methods to your model:
model.state— current state namemodel.is_draft()— returns True if in “draft” statemodel.may_submit()— returns True if the “submit” trigger is valid from current state
may_<trigger>() is especially useful for UI code — show or hide buttons based on available actions.
Common Misconception
“State machines are overkill for simple workflows.” A state machine with 3-4 states and 4-5 transitions is barely more code than the equivalent if/else chain, but it centralizes the rules, prevents invalid states, and makes the workflow self-documenting. The overhead isn’t in the code — it’s in the concept. Once you think in states and transitions, even small workflows benefit.
When to Use Transitions
- Order processing — placed → paid → shipped → delivered
- Document workflows — draft → review → approved → published
- User account lifecycle — active → suspended → banned
- IoT device control — idle → running → error → maintenance
- Game entity behavior — idle → patrolling → chasing → attacking
When Not to Use It
- Simple boolean flags — on/off doesn’t need a state machine
- Unbounded state spaces — if possible states are dynamic or infinite, a state machine isn’t the right model
- Performance-critical inner loops — the library adds method call overhead per transition
One thing to remember: The transitions library takes messy if/else state management and replaces it with a declarative table of states, triggers, and rules — making your workflow’s logic visible, testable, and impossible to violate accidentally.
See Also
- Python Event Emitter Patterns How Python programs shout 'something happened!' so other parts of the code can react — like a school bell that tells everyone it's recess.
- Python Observer Vs Pubsub Two ways Python code can share news — one is like telling your friends directly, the other is like posting on a bulletin board for anyone to read.
- Python Rxpy Reactive Programming How RxPY lets Python code react to streams of data the way a news ticker reacts to breaking stories — automatically and in real time.
- Ci Cd Why big apps can ship updates every day without turning your phone into a glitchy mess — CI/CD is the behind-the-scenes quality gate and delivery truck.
- Containerization Why does software that works on your computer break on everyone else's? Containers fix that — and they're why Netflix can deploy 100 updates a day without the site going down.