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๏ธโƒฃ 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:

    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.state handlers via the donedata parameter:

    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 the done.state.{suffix}
    ๐Ÿ‘€ form โ€” no explicit id= 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'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.

    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# 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:

    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 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): If True, 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...


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_events method, 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_events method lets you query which events have their
    cond/unless guards currently satisfied, going beyond StateMachine.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/**kwargs passed 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 default Model().
    • ๐Ÿ›  Fixes #535 async predicates
      in condition expressions (not, and, or) were not being awaited, causing guards to
      silently return incorrect results.
    • ๐Ÿ›  Fixes #548
      VAR_POSITIONAL and 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 from TransitionList to Event.
    • ๐Ÿ›  Fixes #551 MachineMixin
      now gracefully skips state machine initialization for Django historical models in data
      migrations, instead of raising ValueError.
    • ๐Ÿ›  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.