Python StateMachine v3.0.0 Release Notes
Release Date: 2026-02-24 // 27 days ago-
Upgrading from 2.x? See upgrade guide 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
0๏ธโฃStateChartbase 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.scxmltest 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.Compoundto define them
with Python class syntax โ the class body becomes the state's children:fromstatemachineimportState,StateChartclassShireToRoad(StateChart):classshire(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"}# Truesm.send("visit\_pub")"green\_dragon"insm.configuration\_values# Truesm.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:fromstatemachineimportState,StateChartclassWarOfTheRing(StateChart):classwar(State.Parallel):classfrodos\_quest(State.Compound):shire=State(initial=True)mordor=State(final=True)journey=shire.to(mordor)classaragorns\_path(State.Compound):ranger=State(initial=True)king=State(final=True)coronation=ranger.to(king)sm=WarOfTheRing()"shire"insm.configuration\_valuesand"ranger"insm.configuration\_values# Truesm.send("journey")"mordor"insm.configuration\_valuesand"ranger"insm.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:fromstatemachineimportHistoryState,State,StateChartclassGollumPersonality(StateChart):classpersonality(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"insm.configuration\_values# Truesm.send("leave")sm.send("return\_via\_history")"gollum"insm.configuration\_values# True๐ See history 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:fromstatemachineimportState,StateChartclassBeaconChain(StateChart):classbeacons(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
for full details.DoneData on final states
Final states can provide data to
done.statehandlers via thedonedataparameter:fromstatemachineimportEvent,State,StateChartclassQuestCompletion(StateChart):classquest(State.Compound):traveling=State(initial=True)completed=State(final=True,donedata="get\_result")finish=traveling.to(completed)defget\_result(self):return{"hero":"frodo","outcome":"victory"}epilogue=State(final=True)done\_state\_quest=Event(quest.to(epilogue,on="capture\_result"))defcapture\_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 thedone.state.{suffix}
๐ form โ no explicitid=needed. See done state convention for details.Invoke
States can now spawn external work when entered and cancel it when exited, following the
SCXML<invoke>semantics (similar to UML'sdo/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 fullSignatureAdapterdependency
injection all work out of the box.fromstatemachineimportState,StateChartclassFetchMachine(StateChart):loading=State(initial=True,invoke=lambda: {"status":"ok"})ready=State(final=True)done\_invoke\_loading=loading.to(ready)sm=FetchMachine()importtime;time.sleep(0.1)# wait for background invoke to complete"ready"insm.configuration\_values# TruePassing a list of callables (
invoke=[a, b]) creates independent invocations โ each
sends its owndone.invokeevent, so the first to complete triggers the transition and
cancels the rest. Useinvoke_group()when you need all
callables to complete before transitioning:fromstatemachine.invokeimportinvoke\_groupclassBatchFetch(StateChart):loading=State(initial=True,invoke=invoke\_group(lambda:"a",lambda:"b"))ready=State(final=True)done\_invoke\_loading=loading.to(ready)defon\_enter\_ready(self,data=None,\*\*kwargs):self.results=datasm=BatchFetch()importtime;time.sleep(0.2)sm.results# ['a', 'b']๐ Invoke also supports child state machines (pass a
StateChartsubclass) and SCXML
<invoke>with<finalize>, autoforward, and#_<invokeid>/#_parentsend 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 withevent="error"matcheserror,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 500msDelayed events can be cancelled before they fire using
send_idandcancel_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 eventsA 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()parametersThe
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.executionWhen
catch_errors_as_eventsis enabled (default inStateChart), runtime exceptions during
transitions are caught and...
Previous changes from v2.6.0
-
StateMachine 2.6.0
February 2026
What's new in 2.6.0
๐ This release adds the
StateMachine.enabled_eventsmethod, Python 3.14 support,
๐ a significant performance improvement for callback dispatch, and several bugfixes
for async condition expressions, type checker compatibility, and Django integration.Python compatibility in 2.6.0
๐ StateMachine 2.6.0 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, and 3.14.
Checking enabled events
A new
StateMachine.enabled_eventsmethod lets you query which events have their
cond/unlessguards currently satisfied, going beyondStateMachine.allowed_events
which only checks reachability from the current state.๐ป This is particularly useful for UI scenarios where you want to enable or disable buttons
based on whether an event's conditions are met at runtime.\>\>\>classApprovalMachine(StateMachine): ...pending=State(initial=True) ...approved=State(final=True) ...rejected=State(final=True) ... ...approve=pending.to(approved,cond="is\_manager") ...reject=pending.to(rejected) ... ...is\_manager=False\>\>\>sm=ApprovalMachine()\>\>\>[e.idforeinsm.allowed\_events] ['approve','reject']\>\>\>[e.idforeinsm.enabled\_events()] ['reject']\>\>\>sm.is\_manager=True\>\>\>[e.idforeinsm.enabled\_events()] ['approve','reject']Since conditions may depend on runtime arguments, any
*args/**kwargspassed to
enabled_events()are forwarded to the condition callbacks:\>\>\>classTaskMachine(StateMachine): ...idle=State(initial=True) ...running=State(final=True) ... ...start=idle.to(running,cond="has\_enough\_resources") ... ...defhas\_enough\_resources(self,cpu=0): ...returncpu\>=4\>\>\>sm=TaskMachine()\>\>\>sm.enabled\_events() []\>\>\>[e.idforeinsm.enabled\_events(cpu=8)] ['start']๐ See Checking enabled events in the Guards documentation for more details.
๐ Performance: cached signature binding
Callback dispatch is now significantly faster thanks to cached signature binding in
SignatureAdapter. The first call to a callback computes the argument binding and
caches a fast-path template; subsequent calls with the same argument shape skip the
full binding logic.This results in approximately 60% faster
bind_expected()calls and
around 30% end-to-end improvement on hot transition paths.๐ See #548 for benchmarks.
๐ Bugfixes in 2.6.0
- ๐ Fixes #531 domain model
with falsy__bool__was being replaced by the defaultModel(). - ๐ Fixes #535 async predicates
in condition expressions (not,and,or) were not being awaited, causing guards to
silently return incorrect results. - ๐ Fixes #548
VAR_POSITIONALand kwargs precedence bugs in the signature binding cache introduced
๐ by the performance optimization. - ๐ Fixes #511 Pyright/Pylance
false positive "Argument missing for parameter f" when calling events. Static analyzers
could not follow the metaclass transformation fromTransitionListtoEvent. - ๐ Fixes #551
MachineMixin
now gracefully skips state machine initialization for Django historical models in data
migrations, instead of raisingValueError. - ๐ Fixes #526 sanitize project
๐ path on Windows for documentation builds.
Misc in 2.6.0
- โ Added Python 3.14 support #552.
- โฌ๏ธ Upgraded dev dependencies: ruff to 0.15.0, mypy to 1.14.1
#552. - ๐ Clarified conditional transition evaluation order in documentation
#546. - โ Added pydot DPI resolution settings to diagram documentation
#514. - ๐ Fixed miscellaneous typos in documentation
#522. - โ Removed Python 3.7 from CI build matrix
ef351d5.
- ๐ Fixes #531 domain model