FAQ
Frequently asked questions about XState-StateMachine for Python.
Frequently Asked Questions
General
Is this compatible with XState v5?
Yes. This library is compatible with XState JSON format and supports most XState v4/v5 features, including:
- Hierarchical (nested) states
- Parallel states
invoke(services / actors)- Guards (
guardand legacycondkeys) - Actions (entry, exit, and transition actions)
after(delayed transitions)always(eventless transitions)- Final states
- Context (extended state)
The JSON format exported from Stately.ai works directly with this library.
Can I use this without JSON?
Yes! The Pythonic API (introduced in v0.5.0) lets you define machines entirely in Python without any JSON. There are three styles:
Class-based:
from xstate_statemachine import StateMachine, State, action, guard
class TrafficLight(StateMachine):
green = State(initial=True)
yellow = State()
red = State()
next_event = (
green.to(yellow, event="NEXT")
| yellow.to(red, event="NEXT")
| red.to(green, event="NEXT")
)
@action
def log_transition(self, interpreter, context, event, action_def):
print(f"Transitioned on {event}")
machine = TrafficLight.create_machine()
Builder:
from xstate_statemachine import MachineBuilder
machine = (
MachineBuilder("trafficLight")
.state("green", initial=True)
.state("yellow")
.state("red")
.transition("green", "NEXT", "yellow")
.transition("yellow", "NEXT", "red")
.transition("red", "NEXT", "green")
.build()
)
Functional:
from xstate_statemachine import State, build_machine
green = State("green", initial=True)
yellow = State("yellow")
red = State("red")
green.to(yellow, event="NEXT")
yellow.to(red, event="NEXT")
red.to(green, event="NEXT")
machine = build_machine(id="trafficLight", states=[green, yellow, red])
Which interpreter should I use?
| Use case | Interpreter | Why |
|---|---|---|
| Web servers (FastAPI, aiohttp) | Interpreter (async) |
Works with async event loops |
invoke with async services |
Interpreter (async) |
Services need await |
after (delayed transitions) |
Interpreter (async) |
Timers need an event loop |
| CLI tools and scripts | SyncInterpreter |
No event loop needed |
| Django/Flask views | SyncInterpreter |
Synchronous web frameworks |
| Unit testing | SyncInterpreter |
Deterministic, no timing issues |
| Simple prototyping | SyncInterpreter |
Easier to reason about |
Async example:
import asyncio
from xstate_statemachine import create_machine, Interpreter
async def main():
machine = create_machine(config)
interp = Interpreter(machine)
await interp.start()
await interp.send("EVENT")
await interp.stop()
asyncio.run(main())
Sync example:
from xstate_statemachine import create_machine, SyncInterpreter
machine = create_machine(config)
interp = SyncInterpreter(machine)
interp.start()
interp.send("EVENT")
interp.stop()
Can I use the CLI with machines from Stately.ai?
Yes! Export your machine as JSON from stately.ai, then generate Python code:
xsm gt your_machine.json --template pythonic-class --async-mode no
The CLI reads the XState JSON format that Stately.ai exports and generates complete, runnable Python code.
State Machine Behavior
How do I handle nested state transitions?
Nested (hierarchical) states are fully supported. A transition defined on a parent state automatically catches events from all its children:
{
"id": "app",
"initial": "auth",
"states": {
"auth": {
"initial": "login",
"states": {
"login": {
"on": { "SUBMIT": "verifying" }
},
"verifying": {
"on": { "SUCCESS": "#app.dashboard" }
}
},
"on": {
"CANCEL": "auth.login"
}
},
"dashboard": {}
}
}
Use #machineId.stateName syntax for absolute state references, or relative names for sibling states.
What happens if a guard raises an exception?
If a guard function raises an exception, the transition is blocked (treated as if the guard returned False). If LoggingInspector is attached, the exception is logged:
def risky_guard(context, event):
# If this raises, the transition won't fire
return context["user"]["role"] == "admin" # KeyError if "user" not in context
Tip: Write guards defensively using
.get()or try/except to avoid unexpected blocked transitions.
Can I have multiple machines?
Yes! Create separate MachineNode instances and run them with separate interpreters:
from xstate_statemachine import create_machine, SyncInterpreter
machine_a = create_machine(config_a)
machine_b = create_machine(config_b)
interp_a = SyncInterpreter(machine_a)
interp_b = SyncInterpreter(machine_b)
interp_a.start()
interp_b.start()
interp_a.send("EVENT_FOR_A")
interp_b.send("EVENT_FOR_B")
interp_a.stop()
interp_b.stop()
For parent-child relationships (actor model), use invoke in the parent machine’s config to spawn child machines.
What’s the difference between actions and guards?
| Actions | Guards | |
|---|---|---|
| Purpose | Execute side effects (mutate context, call APIs, log) | Decide whether a transition should happen |
| Return value | None (actions don’t return) | bool — True to allow, False to block |
| Parameters | (interpreter, context, event, action_def) |
(context, event) |
| Async allowed? | Yes (with Interpreter) |
No — always synchronous |
| When called | After the transition is decided | Before the transition is decided |
Testing
How do I test state machines?
Use SyncInterpreter in tests for synchronous, deterministic execution:
import pytest
from xstate_statemachine import create_machine, SyncInterpreter, MachineLogic
def test_light_switch_toggle():
"""Test that toggling switches between on and off."""
config = {
"id": "lightSwitch",
"initial": "off",
"context": {"flips": 0},
"states": {
"off": {"on": {"TOGGLE": {"target": "on", "actions": "increment"}}},
"on": {"on": {"TOGGLE": {"target": "off", "actions": "increment"}}}
}
}
def increment(interpreter, context, event, action_def):
context["flips"] += 1
logic = MachineLogic(actions={"increment": increment})
machine = create_machine(config, logic=logic)
interp = SyncInterpreter(machine)
interp.start()
# Initially off
assert "lightSwitch.off" in interp.current_state_ids
# Toggle to on
interp.send("TOGGLE")
assert "lightSwitch.on" in interp.current_state_ids
assert interp.context["flips"] == 1
# Toggle back to off
interp.send("TOGGLE")
assert "lightSwitch.off" in interp.current_state_ids
assert interp.context["flips"] == 2
interp.stop()
def test_guard_blocks_transition():
"""Test that a guard can prevent a transition."""
config = {
"id": "door",
"initial": "locked",
"context": {"hasKey": False},
"states": {
"locked": {
"on": {"UNLOCK": {"target": "unlocked", "guard": "hasKey"}}
},
"unlocked": {}
}
}
def has_key(context, event):
return context.get("hasKey", False)
logic = MachineLogic(guards={"hasKey": has_key})
machine = create_machine(config, logic=logic)
interp = SyncInterpreter(machine)
interp.start()
# Guard blocks: no key
interp.send("UNLOCK")
assert "door.locked" in interp.current_state_ids
# Give the key and try again
interp.context["hasKey"] = True
interp.send("UNLOCK")
assert "door.unlocked" in interp.current_state_ids
interp.stop()
Environment & Setup
What Python versions are supported?
Python 3.9 through 3.14, with full test coverage across all versions. The library uses no features beyond Python 3.9, ensuring broad compatibility.
Are there any dependencies?
No. Zero external dependencies. The library is pure standard-library Python. You can install it in any environment without dependency conflicts.
How do I persist machine state?
Use the snapshot system to serialize and restore interpreter state:
from xstate_statemachine import create_machine, SyncInterpreter
import json
# Save state
machine = create_machine(config)
interp = SyncInterpreter(machine)
interp.start()
interp.send("SOME_EVENT")
snapshot = interp.get_snapshot()
serialized = json.dumps(snapshot)
# ... store `serialized` in a database, file, or cache ...
# Restore state later
saved_snapshot = json.loads(serialized)
machine2 = create_machine(config)
interp2 = SyncInterpreter(machine2)
interp2.start(snapshot=saved_snapshot)
print(interp2.current_state_ids) # Restored to the saved state
Can I use this with Django/Flask/FastAPI?
Yes!
Django / Flask (synchronous): Use SyncInterpreter:
# Django view example
from xstate_statemachine import create_machine, SyncInterpreter
def checkout_view(request):
machine = create_machine(checkout_config, logic=checkout_logic)
interp = SyncInterpreter(machine)
interp.start()
interp.send("SUBMIT")
return JsonResponse({"state": list(interp.current_state_ids)})
FastAPI (async): Use Interpreter:
# FastAPI endpoint example
from xstate_statemachine import create_machine, Interpreter
@app.post("/checkout")
async def checkout():
machine = create_machine(checkout_config, logic=checkout_logic)
interp = Interpreter(machine)
await interp.start()
await interp.send("SUBMIT")
state = list(interp.current_state_ids)
await interp.stop()
return {"state": state}
Advanced
How do I handle timeouts?
Use after (delayed transitions) with the async Interpreter:
import asyncio
from xstate_statemachine import create_machine, Interpreter
config = {
"id": "session",
"initial": "active",
"states": {
"active": {
"after": {
"30000": "timed_out"
},
"on": { "ACTIVITY": "active" }
},
"timed_out": { "type": "final" }
}
}
async def main():
machine = create_machine(config)
interp = Interpreter(machine)
await interp.start()
# Machine will transition to "timed_out" after 30 seconds of inactivity
# Sending "ACTIVITY" resets the timer by re-entering "active"
Warning:
aftertransitions are not supported bySyncInterpreter. Use the asyncInterpreterfor delayed transitions.
Can I use this for UI state management?
Yes. State machines are excellent for managing UI state — form wizards, modals, navigation flows, loading states, etc. While this is a Python library (not JavaScript), it works well for:
- Server-side rendered UI state (Django templates, Jinja2)
- API-driven UI state (send state to frontend via JSON)
- Desktop apps (Tkinter, PyQt, etc.)
How do I debug my state machine?
See the Troubleshooting page for detailed debugging strategies. Quick summary:
- Attach
LoggingInspectorto see all transitions - Print
interp.current_state_idsafter each event - Print
interp.contextto verify data flow - Use
machine.to_mermaid()to visualize the machine - Use
SyncInterpreterfor deterministic, step-by-step debugging
Is there a visual editor?
Yes — Stately.ai is the visual editor for XState machines. Design your machine visually, export as JSON, and use the xsm CLI to generate Python code:
xsm gt exported_machine.json --template pythonic-class
What’s the performance overhead?
The library is lightweight with minimal overhead:
- Machine creation: microseconds for typical configs
- Event processing: microseconds per transition
- Memory: proportional to the number of states and transitions
- No background threads or event loops (unless using
Interpreterwithafter)
For most applications, the state machine overhead is negligible compared to your business logic (database queries, API calls, etc.).
Can I extend the library?
Yes. The plugin system allows you to hook into the interpreter lifecycle:
from xstate_statemachine import PluginBase
class MetricsPlugin(PluginBase):
def on_transition(self, event, source, target):
metrics.increment(f"transition.{source}.{target}")
def on_event(self, event):
metrics.increment(f"event.{event.type}")
interp.use(MetricsPlugin())
You can also subclass MachineLogic for custom logic loading, or create your own LogicLoader subclass for alternative discovery strategies.