--- layout: home title: Home ---
v{{ site.version }} is out — release notes

State machines for Python, done right.

Run your XState or Stately JSON unmodified, or define machines in plain Python. Async and sync engines share one algorithm. A terminal toolkit that generates, inspects and simulates machines — and proves its generated code before it writes a file. Zero dependencies.

$ pip install xstate-statemachine
  • 9,000+ tests
  • 3.9–3.15 Python
  • 0 dependencies
  • MIT licensed
order.machine.json live
pending submitted open filled cancelled
state pending event —

Everything a statechart runtime should do.
Nothing it shouldn't.

XState JSON, unmodified

Export from Stately and run it. Nested, parallel, history, guards, delays, invoke, actors, tags, meta.

from xstate_statemachine import create_machine, Interpreter
machine = create_machine(json.load(open("order.json")))
interp  = await Interpreter(machine).start()

One algorithm, two engines

Interpreter for asyncio, SyncInterpreter for everything else. Same core; cannot drift.

Time you control

Inject a Clock. SimulatedClock fires a 30‑second timeout in microseconds.

Ask, don't poll

await send("SUBMIT", wait=True) hands you a Receipt the moment the step has run. send_priority jumps the queue; the inbox is bounded with backpressure.

Receipt state_ids={'submitted'} changed=True error=None deferred=False denied=False

Persistence that resumes

Built-in MemoryStore / FileStore / SQLiteStore (Redis as an extra) behind one persisted() loop: optimistic or fenced locks so two workers never lose an update, an idempotency inbox that answers a replayed webhook with the original receipt, durable after timers a scanner wakes after a restart. What is exactly-once and what is not is written down on the Guarantees page.

Web-ready, closed by default

[fastapi], [starlette], [litestar], [flask] (with its Quart shim), [django], [channels] and [drf] turn a chart into routes: one POST per event (OpenAPI from your Pydantic events on FastAPI and Litestar, drf-spectacular on DRF), receipts mapped to 200/409/422, SSE, WebSocket and Django Channels streams, a Django admin with CSRF-protected transition buttons, [sqlalchemy] columns and stores. authorize= is required, error bodies never leak exception text, and the core stays zero-dependency.

Seen, traced, tested

[observability] emits OpenTelemetry spans and Prometheus metrics with label hygiene, structlog/loguru/Sentry hooks; a stdlib live inspector speaks the Stately wire protocol. [testing] ships pytest fixtures, state and transition coverage, Hypothesis strategies and replay() for audit logs.

Typed context

context_type=MyCtx flows a TypedDict to interp.context. A typo'd key is a checker error.

A terminal toolkit

xsm generates typed Python (plus recorded tests, typed context, a plugin skeleton), inspects, simulates, diagrams and documents your machines. An interactive launcher on a terminal, plain text when piped — still zero dependencies.

Docs that run

Every Python block in the guide and the README is executed in CI. Every link is resolved. 78 runnable examples.

LLM agents, governed

The model proposes, the machine decides: per-state tool allow-lists, budgets, timeouts and durable human approval. Agents guide · vs LangGraph

Four ways to define a machine

Pick the style that matches your team. All compile to the same runtime.

{% highlight python %} from xstate_statemachine import ( State, StateMachine, SyncInterpreter, action, guard ) class TrafficLight(StateMachine): machine_id = "trafficLight" # States green = State("green", initial=True) yellow = State("yellow") red = State("red") # Transitions slow_down = green.to(yellow, event="TIMER") stop = yellow.to(red, event="TIMER") go = red.to(green, event="TIMER") @action def log_change(self, interpreter, context, event, action_def): print(f"Light: {interpreter.active_state_ids}") # Run it machine = TrafficLight.create_machine() interp = SyncInterpreter(machine).start() interp.send("TIMER") # green → yellow interp.send("TIMER") # yellow → red interp.stop() {% endhighlight %}
{% highlight python %} from xstate_statemachine import MachineBuilder, SyncInterpreter machine = ( MachineBuilder("trafficLight") .state("green", initial=True) .state("yellow") .state("red") .transition("green", "TIMER", "yellow") .transition("yellow", "TIMER", "red") .transition("red", "TIMER", "green") .build() ) interp = SyncInterpreter(machine).start() interp.send("TIMER") # green → yellow interp.send("TIMER") # yellow → red interp.stop() {% endhighlight %}
{% highlight python %} from xstate_statemachine import State, build_machine, SyncInterpreter green = State( "green", initial=True, on={ "TIMER": "yellow", }, ) yellow = State( "yellow", on={ "TIMER": "red", }, ) red = State( "red", on={ "TIMER": "green", }, ) machine = build_machine( id="trafficLight", states=[green, yellow, red], ) interp = SyncInterpreter(machine).start() interp.send("TIMER") # green → yellow interp.send("TIMER") # yellow → red interp.stop() {% endhighlight %}
{% highlight python %} from xstate_statemachine import create_machine, SyncInterpreter config = { "id": "trafficLight", "initial": "green", "states": { "green": { "on": { "TIMER": "yellow", }, }, "yellow": { "on": { "TIMER": "red", }, }, "red": { "on": { "TIMER": "green", }, }, }, } machine = create_machine(config) interp = SyncInterpreter(machine).start() interp.send("TIMER") # green → yellow interp.send("TIMER") # yellow → red interp.stop() {% endhighlight %}

How it compares

Against the three other Python state-machine libraries in common use — transitions 0.9.3, python-statemachine 3.2.2, sismic 1.6.14, checked against the installed packages on 2026-11-07. A dash means the capability exists in a limited form (a thread-based timeout extension, an optional-dependency diagram, listener callbacks rather than a plugin API).

Full pages, with a side-by-side order lifecycle and when to choose each: vs transitions · vs python-statemachine · vs django-fsm.

Feature XState-StateMachine transitions python-statemachine sismic
Hierarchical states
Parallel regions
Delayed transitions (after)
Invoked services / child machines
Actor model (spawn, sendTo, escalate)
XState v5 JSON compatibility
Declarative definition (JSON / YAML / SCXML)
Async and sync engines
Virtual-time clock for tests
Deep snapshot / restore
Bounded inbox with overflow policy
Build-time validation
Plugin / inspection hooks
CLI: generate, inspect, simulate, diagram
Diagram export
Zero runtime dependencies

How It Performs

Same machine shape, each library through its own idiomatic API. Higher is better; bold is fastest in the row (two bold cells: within 3 %, a tie).

Scenario XState-StateMachine transitions python-statemachine sismic
Flat toggle events/s95,618142,0799,46613,084
Guard + action events/s164,764101,1018,18412,595
3-level nested events/s41,3758,4723,8425,133
Parallel regions events/s30,9726,0333,8884,750
Delayed transitions timers/s9,337706126,119
1,000 instances instances/s51,13538,3113,4757,956
Construction machines/s12,2739,3351,461285
Native asyncio events/s26,61439,1176,870—

transitions is faster on the flattest machine (a two-state toggle, 1.35× ours) and on native asyncio (1.43×) because it is a transition table, not a statechart engine — no configuration set, no entry/exit ordering, no internal queue. Add a guard and an action and the row is ours (1.63×, since the sync send() fast paths); the moment states nest or run in parallel, that algorithm is what the others have to emulate, and XState-StateMachine is 4.8–5.2× faster than the next library. Construction is 31 % ahead of transitions and fanning out to 1,000 instances is 33 % ahead of transitions — while still running the full build-time validator on every create_machine(), and 6–43× ahead of the other two on both rows. Measured 2026-11-07 on 0.12.1 against the latest releases (transitions 0.9.3, python-statemachine 3.2.2, sismic 1.6.14; three quiet sessions, the one closest to their medians shown), all libraries in one session: Python 3.14, median of 7 runs, GC disabled, setup excluded. Reproduce with benchmarks/competitors/run.py; full table and caveats in the benchmark README, our own scaling curves in Production Characteristics.

Ship state you can reason about.

Start with the guide, or read the whole API on one page.