StateMachine 3.0.0¶
February 24, 2026
See also
Upgrading from 2.x? See Upgrading from 2.x to 3.0 for a step-by-step migration guide.
What’s new in 3.0.0¶
Statecharts are here! 🎉
Version 3.0 brings full statechart support to the library — compound states, parallel states,
history pseudo-states, and an SCXML-compliant processing model. It also introduces a new
StateChart base class with modern defaults, a richer event dispatch system (delayed events,
internal queues, cancellation), structured error handling, and several developer-experience
improvements.
The implementation follows the SCXML specification (W3C),
which defines a standard for statechart semantics. This ensures predictable behavior on
edge cases and compatibility with other SCXML-based tools. The automated test suite now
includes W3C-provided .scxml test cases to verify conformance.
While this is a major version with backward-incompatible changes, the existing StateMachine
class preserves 2.x defaults. See the
upgrade guide for a smooth migration path.
Compound states¶
Compound states have inner child states. Use State.Compound to define them
with Python class syntax — the class body becomes the state’s children:
>>> from statemachine import State, StateChart
>>> class ShireToRoad(StateChart):
... class shire(State.Compound):
... bag_end = State(initial=True)
... green_dragon = State()
... visit_pub = bag_end.to(green_dragon)
...
... road = State(final=True)
... depart = shire.to(road)
>>> sm = ShireToRoad()
>>> set(sm.configuration_values) == {"shire", "bag_end"}
True
>>> sm.send("visit_pub")
>>> "green_dragon" in sm.configuration_values
True
>>> sm.send("depart")
>>> set(sm.configuration_values) == {"road"}
True
Entering a compound activates both the parent and its initial child. Exiting removes the parent and all descendants. See Compound states for full details.
Parallel states¶
Parallel states activate all child regions simultaneously. Use State.Parallel:
>>> from statemachine import State, StateChart
>>> class WarOfTheRing(StateChart):
... class war(State.Parallel):
... class frodos_quest(State.Compound):
... shire = State(initial=True)
... mordor = State(final=True)
... journey = shire.to(mordor)
... class aragorns_path(State.Compound):
... ranger = State(initial=True)
... king = State(final=True)
... coronation = ranger.to(king)
>>> sm = WarOfTheRing()
>>> "shire" in sm.configuration_values and "ranger" in sm.configuration_values
True
>>> sm.send("journey")
>>> "mordor" in sm.configuration_values and "ranger" in sm.configuration_values
True
Events in one region don’t affect others. See Parallel states for full details.
History pseudo-states¶
The History pseudo-state records the configuration of a compound state when it
is exited. Re-entering via the history state restores the previously active child.
Supports both shallow (HistoryState()) and deep (HistoryState(type="deep")) history:
>>> from statemachine import HistoryState, State, StateChart
>>> class GollumPersonality(StateChart):
... class personality(State.Compound):
... smeagol = State(initial=True)
... gollum = State()
... h = HistoryState()
... dark_side = smeagol.to(gollum)
... light_side = gollum.to(smeagol)
... outside = State()
... leave = personality.to(outside)
... return_via_history = outside.to(personality.h)
>>> sm = GollumPersonality()
>>> sm.send("dark_side")
>>> "gollum" in sm.configuration_values
True
>>> sm.send("leave")
>>> sm.send("return_via_history")
>>> "gollum" in sm.configuration_values
True
See History pseudo-states for full details on shallow vs deep history.
Eventless (automatic) transitions¶
Transitions without an event trigger fire automatically when their guard condition is met:
>>> from statemachine import State, StateChart
>>> class BeaconChain(StateChart):
... class beacons(State.Compound):
... first = State(initial=True)
... second = State()
... last = State(final=True)
... first.to(second)
... second.to(last)
... signal_received = State(final=True)
... done_state_beacons = beacons.to(signal_received)
>>> sm = BeaconChain()
>>> set(sm.configuration_values) == {"signal_received"}
True
The entire eventless chain cascades in a single macrostep. See Eventless (automatic) transitions for full details.
DoneData on final states¶
Final states can provide data to done.state handlers via the donedata parameter:
>>> from statemachine import Event, State, StateChart
>>> class QuestCompletion(StateChart):
... class quest(State.Compound):
... traveling = State(initial=True)
... completed = State(final=True, donedata="get_result")
... finish = traveling.to(completed)
... def get_result(self):
... return {"hero": "frodo", "outcome": "victory"}
... epilogue = State(final=True)
... done_state_quest = Event(quest.to(epilogue, on="capture_result"))
... def capture_result(self, hero=None, outcome=None, **kwargs):
... self.result = f"{hero}: {outcome}"
>>> sm = QuestCompletion()
>>> sm.send("finish")
>>> sm.result
'frodo: victory'
The done_state_ naming convention automatically registers the done.state.{suffix}
form — no explicit id= needed. See done_state_ prefix for details.
Invoke¶
States can now spawn external work when entered and cancel it when exited, following the
SCXML <invoke> semantics (similar to UML’s do/ activity). Handlers run in a daemon
thread (sync engine) or a thread executor wrapped in an asyncio Task (async engine).
Invoke is a first-class callback group — convention naming (on_invoke_<state>),
decorators (@state.invoke), inline callables, and the full SignatureAdapter dependency
injection all work out of the box.
>>> from statemachine import State, StateChart
>>> class FetchMachine(StateChart):
... loading = State(initial=True, invoke=lambda: {"status": "ok"})
... ready = State(final=True)
... done_invoke_loading = loading.to(ready)
>>> sm = FetchMachine()
>>> import time; time.sleep(0.1) # wait for background invoke to complete
>>> "ready" in sm.configuration_values
True
Passing a list of callables (invoke=[a, b]) creates independent invocations — each
sends its own done.invoke event, so the first to complete triggers the transition and
cancels the rest. Use invoke_group() when you need all
callables to complete before transitioning:
>>> from statemachine.invoke import invoke_group
>>> class BatchFetch(StateChart):
... loading = State(initial=True, invoke=invoke_group(lambda: "a", lambda: "b"))
... ready = State(final=True)
... done_invoke_loading = loading.to(ready)
...
... def on_enter_ready(self, data=None, **kwargs):
... self.results = data
>>> sm = BatchFetch()
>>> import time; time.sleep(0.2)
>>> sm.results
['a', 'b']
Invoke also supports child state machines (pass a StateChart subclass) and SCXML
<invoke> with <finalize>, autoforward, and #_<invokeid> / #_parent send targets
for parent-child communication.
See Invoke for full documentation.
Event dispatch¶
Event matching following SCXML spec¶
Event matching now follows the SCXML spec — a
transition’s event descriptor is a prefix match against the dot-separated event name. For
example, a transition with event="error" matches error, error.send,
error.send.failed, etc.
An event designator consisting solely of * can be used as a wildcard matching any event.
See Events for full details.
Delayed events¶
Events can be scheduled for future processing using delay (in milliseconds). The engine
tracks execution time and processes the event only when the delay has elapsed.
sm.send("light_beacons", delay=500) # fires after 500ms
Delayed events can be cancelled before they fire using send_id and cancel_event().
Cancellation is most useful in async codebases, where other coroutines can cancel the
event while the delay is pending. In the sync engine, the delay is blocking — the
processing loop sleeps until the delay elapses.
sm.send("light_beacons", delay=5000, send_id="beacon_signal")
sm.cancel_event("beacon_signal") # cancel from another coroutine or callback
See Delayed events for details.
raise_() — internal events¶
A new raise_() method sends events to the internal queue, equivalent to
send(..., internal=True). Internal events are processed immediately within the current
macrostep, before any external events. See Sending events.
New send() parameters¶
The send() method now accepts additional optional parameters:
delay(float): Time in milliseconds before the event is processed.send_id(str): Identifier for the event, useful for cancelling delayed events.internal(bool): IfTrue, the event is placed in the internal queue and processed in the current macrostep.
Existing calls to send() are fully backward compatible.
Error handling with error.execution¶
When catch_errors_as_events is enabled (default in StateChart), runtime exceptions during
transitions are caught and result in an internal error.execution event. This follows
the SCXML error handling specification.
A naming convention makes this easy to use: any event attribute starting with error_
automatically matches both the underscore and dot-notation forms:
>>> from statemachine import State, StateChart
>>> class MyChart(StateChart):
... s1 = State("s1", initial=True)
... error_state = State("error_state", final=True)
...
... go = s1.to(s1, on="bad_action")
... error_execution = s1.to(error_state) # matches "error.execution" automatically
...
... def bad_action(self):
... raise RuntimeError("something went wrong")
>>> sm = MyChart()
>>> sm.send("go")
>>> sm.configuration == {sm.error_state}
True
Errors are caught at the block level: each microstep phase (exit, transition on,
enter) is an independent block. An error in one block does not prevent subsequent blocks
from executing — in particular, after callbacks always run, making after_<event>() a
natural finalize hook.
The error object is available as error in handler kwargs. See How errors are caught
for full details.
New API¶
configuration and configuration_values¶
Due to compound and parallel states, the state machine can now have multiple active states.
The new configuration property returns an OrderedSet[State] of all currently active
states, and configuration_values returns their values. These replace the deprecated
current_state property. See Querying the configuration.
is_terminated property¶
A new read-only property that returns True when the state machine has reached a final
state and the engine is no longer running. Works correctly for all topologies — flat,
compound, and parallel. See Checking termination.
>>> from statemachine import State, StateChart
>>> class SimpleSM(StateChart):
... idle = State(initial=True)
... done = State(final=True)
... finish = idle.to(done)
>>> sm = SimpleSM()
>>> sm.is_terminated
False
>>> sm.send("finish")
>>> sm.is_terminated
True
In(state) condition checks¶
Conditions can now check if a state is in the current configuration using the
In('<state-id>') syntax. This is particularly useful in parallel regions where
a transition depends on the state of another region. See Condition expressions.
>>> from statemachine import State, StateChart
>>> class Spaceship(StateChart):
... class systems(State.Parallel):
... class engine(State.Compound):
... off = State(initial=True)
... on = State()
... ignite = off.to(on)
... class hatch(State.Compound):
... open = State(initial=True)
... sealed = State()
... seal = open.to(sealed)
... orbit = State(final=True)
... launch = systems.to(orbit, cond="In('on') and In('sealed')")
>>> sm = Spaceship()
>>> sm.send("launch") # engine off, hatch open — guard fails
>>> "off" in sm.configuration_values
True
>>> sm.send("ignite")
>>> sm.send("launch") # engine on, hatch still open — guard fails
>>> "on" in sm.configuration_values and "open" in sm.configuration_values
True
>>> sm.send("seal")
>>> sm.send("launch") # both conditions met — launches!
>>> sm.is_terminated
True
prepare_event() callback¶
The prepare_event callback lets you inject custom data into **kwargs for all
other callbacks in the same event processing cycle. See Preparing events.
>>> from statemachine import State, StateMachine
>>> class ExampleStateMachine(StateMachine):
... initial = State(initial=True)
...
... loop = initial.to.itself()
...
... def prepare_event(self):
... return {"foo": "bar"}
...
... def on_loop(self, foo):
... return f"On loop: {foo}"
>>> sm = ExampleStateMachine()
>>> sm.loop()
'On loop: bar'
Constructor kwargs forwarded to initial state callbacks¶
Constructor keyword arguments are forwarded to initial state callbacks, so self-contained machines can receive context at creation time:
>>> from statemachine import State, StateChart
>>> class Greeter(StateChart):
... idle = State(initial=True)
... done = State(final=True)
... idle.to(done)
...
... def on_enter_idle(self, name=None, **kwargs):
... self.greeting = f"Hello, {name}!"
>>> sm = Greeter(name="Alice")
>>> sm.greeting
'Hello, Alice!'
Developer experience¶
StateChart base class¶
The new StateChart class is the recommended base for all new state machines. It enables
SCXML-compliant defaults: catch_errors_as_events, enable_self_transition_entries, and
non-atomic configuration updates. The existing StateMachine class is now a subclass with
backward-compatible defaults. See Behaviour for a comparison table.
Typed models with Generic[TModel]¶
StateChart now supports a generic type parameter for the model, enabling full type
inference and IDE autocompletion on sm.model:
>>> from statemachine import State, StateChart
>>> class MyModel:
... name: str = ""
... value: int = 0
>>> class MySM(StateChart["MyModel"]):
... idle = State(initial=True)
... active = State(final=True)
... go = idle.to(active)
>>> sm = MySM(model=MyModel())
>>> sm.model.name
''
With this declaration, type checkers infer sm.model as MyModel (not Any), so
accessing sm.model.name or sm.model.value gets full autocompletion and type safety.
When no type parameter is given, StateChart defaults to StateChart[Any] for backward
compatibility. See Domain models for details.
Improved type checking with pyright¶
The library now supports pyright in addition to mypy.
Type annotations have been improved throughout the codebase, and a catch-all __getattr__
that previously returned Any has been removed — type checkers can now detect misspelled
attribute names and unresolved references on StateChart subclasses.
Self-transition entry/exit behavior¶
In StateChart, self-transitions now execute entry and exit actions, following the SCXML
spec. The enable_self_transition_entries class attribute controls this behavior.
StateMachine preserves the 2.x default (no entry/exit on self-transitions).
See Self-transitions and internal transitions.
Class-level listener declarations¶
Listeners can now be declared at the class level using the listeners attribute, so they are
automatically attached to every instance. The list accepts callables (classes, partial, lambdas)
as factories that create a fresh listener per instance, or pre-built instances that are shared.
A setup() protocol allows factory-created listeners to receive runtime dependencies
(DB sessions, Redis clients, etc.) via **kwargs forwarded from the SM constructor.
Inheritance is supported: child listeners are appended after parent listeners, unless
listeners_inherit = False is set to replace them entirely.
See Listeners for full documentation.
Weighted (probabilistic) transitions¶
A new contrib module statemachine.contrib.weighted provides weighted_transitions(),
enabling probabilistic transition selection based on relative weights. This works entirely
through the existing condition system — no engine changes required.
See Weighted transitions for full documentation.
State timeouts¶
A new contrib module statemachine.contrib.timeout provides a timeout() invoke helper
for per-state watchdog timers. When a state is entered, a background timer starts; if the
state is not exited before the timer expires, an event is sent automatically. The timer is
cancelled on state exit, with no manual cleanup needed.
>>> from statemachine import State, StateChart
>>> from statemachine.contrib.timeout import timeout
>>> class WaitingMachine(StateChart):
... waiting = State(initial=True, invoke=timeout(5, on="expired"))
... timed_out = State(final=True)
... expired = waiting.to(timed_out)
>>> sm = WaitingMachine()
>>> sm.waiting.is_active
True
See State timeouts for full documentation.
Create state machine from a dict definition¶
Dynamically create state machine classes using
create_machine_class_from_definition():
>>> from statemachine.io import create_machine_class_from_definition
>>> machine = create_machine_class_from_definition(
... "TrafficLightMachine",
... **{
... "states": {
... "green": {"initial": True, "on": {"change": [{"target": "yellow"}]}},
... "yellow": {"on": {"change": [{"target": "red"}]}},
... "red": {"on": {"change": [{"target": "green"}]}},
... },
... }
... )
>>> sm = machine()
>>> sm.green.is_active
True
>>> sm.send("change")
>>> sm.yellow.is_active
True
Async concurrent event result routing¶
When multiple coroutines send events concurrently via asyncio.gather, each
caller now receives its own event’s result (or exception). Previously, only the
first caller to acquire the processing lock would get a result — subsequent
callers received None and exceptions could leak to the wrong caller.
This is implemented by attaching an asyncio.Future to each externally
enqueued event in the async engine. See Async support for details.
Fixes #509.
Migration guide from pytransitions¶
A new Coming from pytransitions guide helps users of the transitions library evaluate the differences and migrate their state machines. It includes side-by-side code comparisons and a feature matrix.
Migration guide from the GoF State Pattern¶
A new Coming from the State Pattern guide helps developers familiar with the classic Gang of Four State Pattern understand how to port their hand-rolled state implementations to python-statemachine. It walks through a complete example, compares the two approaches, and highlights what you gain from the declarative style.
Validation flags¶
The strict_states class parameter has been replaced by two independent, always-on
class-level attributes:
validate_trap_states: non-final states must have at least one outgoing transition.validate_final_reachability: when final states exist, all non-final states must have a path to at least one final state.validate_disconnected_states: all states must be reachable from the initial state.
See Validations for details.
Known limitations¶
The following SCXML features are not yet implemented and are deferred to a future release:
HTTP and other external communication targets (only
#_internal,#_parent, and#_<invokeid>send targets are supported)
See also
For a step-by-step migration guide with before/after examples, see Upgrading from 2.x to 3.0.
Backward incompatible changes in 3.0¶
This section summarizes the breaking changes. For detailed before/after examples and migration instructions, see the upgrade guide.
Python 3.7 and 3.8 dropped. StateMachine 3.0 supports Python 3.9 through 3.14.
Non-RTC model removed. The
rtcparameter (deprecated since 2.3.2) has been removed. All events are now queued before being processed.current_statedeprecated. Useconfiguration/configuration_valuesinstead. With compound and parallel states, multiple states can be active simultaneously.Configuration update timing. In
StateChart, states are exited beforeoncallbacks and entered after, following the SCXML spec. Two new kwargs —previous_configurationandnew_configuration— are available inoncallbacks. Useatomic_configuration_update=Trueor theStateMachineclass to restore the 2.x behavior.Self-transition entry/exit. In
StateChart, self-transitions now triggeron_enter_*/on_exit_*callbacks. Setenable_self_transition_entries = Falseto restore the old behavior.add_observer()removed. Useadd_listener()instead.TransitionNotAllowedchanges. Now storesconfiguration(a set) instead ofstate, andeventcan beNone.allow_event_without_transitionmoved to class level. No longer an__init__parameter.States.from_enumdefault changed.use_enum_instancenow defaults toTrue.Short registry names removed. Use fully-qualified names with
get_machine_cls().strict_statesremoved. Replaced byvalidate_trap_statesandvalidate_final_reachability(both default toTrue).__repr__output changed. Now showsconfiguration=[...]instead ofcurrent_state=....