All Versions
15
Latest Version
Avg Release Cycle
87 days
Latest Release
22 days ago

Changelog History
Page 1

  • v3.1.2 Changes

    May 19, 2026

    🐛 Bug fixes in 3.1.2

    pydot is optional again

    Starting in 3.1.0, defining any StateMachine or StateChart subclass
    implicitly required pydot to be installed. The metaclass invokes
    _expand_docstring() for every class, and that path eagerly imported the
    diagram formatter, which in turn imported pydot at module load time, so
    import of a user module failed with ModuleNotFoundError: No module named 'pydot' even when no diagrams were rendered.

    ⏪ Two changes restore the original behavior:

    • _expand_docstring now short-circuits when the class docstring contains
      no {statechart:FORMAT} placeholder, skipping the formatter import for
      the common case.
    • statemachine.contrib.diagram no longer re-exports renderer classes at
      📦 the package top level, and renderer modules are only imported when
      actually used. The dependency is only resolved when dot/svg
      rendering is requested. Mermaid and Markdown/RST formats work without
      pydot.

    If your code imports DotRenderer, DotRendererConfig, MermaidRenderer
    or MermaidRendererConfig directly from statemachine.contrib.diagram,
    import them from the renderer submodule instead:

    fromstatemachine.contrib.diagram.renderers.dotimportDotRendererfromstatemachine.contrib.diagram.renderers.mermaidimportMermaidRenderer
    

    #622.

    Other changes in 3.1.2

    🔒 Security and dependency bumps

    Picked up from the dependency refresh applied to main:

    • ruff >=0.15.13
    • ✅ pytest >=9.0.3 (CVE-2025, tmpdir handling) for py>=3.10
    • django >=5.2.14 for py3.10/3.11 (multiple CVEs)
    • pillow >=12.2.0 for py>=3.10 (multiple CVEs)
    • transitive: urllib3 2.6.3 -> 2.7.0 (GHSA-qccp-gfcp-xxvc), requests
      2.33.1 -> 2.34.2

    CI workflows: actions/checkout v4 → v5, actions/setup-python v5 → v6,
    📌 astral-sh/setup-uv v3 → v8 (pinned to v8.1.0), codecov/codecov-action
    v4 → v6.

  • v3.1.1 Changes

    May 16, 2026

    StateChart 3.1.1

    May 15, 2026

    🐛 Bug fixes in 3.1.1

    🔧 Thread-safety hardening of the configuration cache

    🔧 Two races in Configuration (introduced indirectly by the cache + no-copy
    🛠 design in 3.1.0) have been fixed. Both surfaced under concurrent reads of
    🔧 machine.configuration while another thread is sending events to the same
    🔀 state machine instance, a scenario explicitly supported by the sync engine.

    1. Cache read race. Configuration.states checked
      self._cached is not None and then returned self._cached. Another
      thread invalidating between the check and the return could cause the
      property to return None, leading to a TypeError in callers that
      🔧 iterate the result (e.g., list(machine.configuration)). The getter now
      snapshots the cache fields locally before the freshness check.
      #620.

    2. In-place mutation race. Configuration.add() and
      🔧 Configuration.discard() mutated the OrderedSet stored on the model
      in place and rewrote the same reference. A concurrent reader iterating
      🔧 .configuration could observe a partially mutated set (raising
      RuntimeError: Set changed size during iteration) or read back a stale
      cached resolution missing the new state. Both methods now use
      copy-on-write, producing a fresh OrderedSet per call. This affects
      🔧 only StateChart (where atomic_configuration_update=False is the
      ⚡️ default to support parallel regions). The atomic update path used by
      StateMachine was never affected.
      #620.

    ✅ Both fixes are covered by new stress tests in
    tests/test_threading.py::TestThreadSafety:
    test_concurrent_send_and_read_configuration and
    test_concurrent_parallel_region_send_and_read, plus a deterministic
    ➕ copy-on-write contract test test_add_discard_produce_fresh_orderedset.

    🐎 Performance impact

    Copy-on-write in add() / discard() reintroduces an O(n) shallow copy of
    🔧 the active configuration on every state entry and exit. For the typical
    🔧 configuration sizes used in practice (1–7 states), this is sub-microsecond.

    🍎 Measured on macOS / Python 3.14, pytest-benchmark median, vs 3.1.0:

    Benchmark 3.1.0 3.1.1 Δ
    test_parallel_region_events 175.2 μs 184.5 μs +5.3%
    test_many_transitions_reset 125.9 μs 139.5 μs +10.9%
    test_guarded_transitions 70.0 μs 75.7 μs +8.2%
    test_history_pause_resume 88.4 μs 91.4 μs +3.4%
    test_many_transitions_full_cycle 156.9 μs 162.1 μs +3.3%
    test_flat_self_transition 38.7 μs 39.1 μs +1.0%

    Overall 4.7x–7.7x event throughput improvement vs 3.0.0 (declared in
    🚀 3.1.0 release notes)
    is unchanged.

  • v3.1.0 Changes

    May 15, 2026

    StateChart 3.1.0

    May 15, 2026

    📄 📘 Full docs: https://python-statemachine.readthedocs.io/en/v3.1.0/

    What's new in 3.1.0

    Text representations with format()

    👍 State machines now support Python's built-in format() protocol. Use f-strings
    or format() to get text representations — on both classes and instances:

    f"{TrafficLightMachine:md}"f"{sm:mermaid}"format(sm,"rst")
    

    👌 Supported formats:

    Format Output Requires
    dot Graphviz DOT source pydot
    svg SVG markup (via Graphviz) pydot + graphviz
    mermaid Mermaid stateDiagram-v2
    md Markdown transition table
    rst RST transition table

    👀 See Text representations for details.

    Formatter facade

    A new Formatter facade with decorator-based registration unifies all text
    format rendering behind a single API. Adding a new format requires only
    registering a render function — no changes to __format__, the CLI, or the
    Sphinx directive:

    fromstatemachine.contrib.diagramimportformatterformatter.render(sm,"mermaid")formatter.supported\_formats()@formatter.register\_format("custom")def\_render\_custom(machine\_or\_class):
        ...
    

    👀 See Using the formatter API for details.

    👍 Mermaid diagram support

    State machines can now be rendered as
    Mermaid stateDiagram-v2
    👍 source text — no Graphviz installation required. Supports compound states,
    parallel regions, history states, guards, and active-state highlighting.

    Three ways to use it:

    • f-strings: f"{sm:mermaid}"
    • CLI: python -m statemachine.contrib.diagram MyMachine - --format mermaid
    • Sphinx directive: :format: mermaid renders via sphinxcontrib-mermaid.

    👀 See Mermaid format for details.

    📄 Auto-expanding docstrings

    📄 Use {statechart:FORMAT} placeholders in your class docstring to embed a
    live representation of the state machine. The placeholder is replaced at
    🔀 class definition time, so the docstring always stays in sync with the code:

    classTrafficLight(StateChart):"""A traffic light. {statechart:md} """green=State(initial=True)yellow=State()red=State()cycle=green.to(yellow)|yellow.to(red)|red.to(green)
    

    Any registered format works: md, rst, mermaid, dot, etc.
    📄 Works with Sphinx autodoc — the expanded docstring is what gets rendered.
    👀 See Auto-expanding docstrings for details.

    Sphinx directive for inline diagrams

    A new Sphinx extension renders state machine diagrams directly in your
    📚 documentation from an importable class path — no manual image generation
    needed.

    ➕ Add "statemachine.contrib.diagram.sphinx_ext" to your conf.py
    extensions, then use the directive in any MyST Markdown page:

    ```{statemachine-diagram}myproject.machines.OrderControl:events: receive\_payment:caption: After payment:target:```
    

    👍 The directive supports the same options as the standard image/figure
    directives (:width:, :height:, :scale:, :align:, :target:,
    :class:, :name:), plus :events: to instantiate the machine and send
    events before rendering (highlighting the current state).

    Using :target: without a value makes the diagram clickable, opening the
    💻 full SVG in a new browser tab for zooming — useful for large statecharts.

    The :format: mermaid option renders via sphinxcontrib-mermaid instead of
    Graphviz.

    📚 See Sphinx directive for full documentation.
    #589.

    Diagram CLI --events and --format options

    The python -m statemachine.contrib.diagram command now accepts:

    • --events to instantiate the machine and send events before rendering,
      highlighting the current active state.
    • --format to choose the output format (mermaid, md, rst, dot, svg,
      or image formats via Graphviz). Use - as the output path to write text
      formats to stdout.

    👀 See Command line for details.
    #593.

    🐎 Performance: 5x–7x faster event processing

    ⚡️ The engine's hot paths have been systematically profiled and optimized, resulting in
    4.7x–7.7x faster event throughput and 1.9x–2.6x faster setup across all
    machine types. All optimizations are internal — no public API changes.
    👀 See #592 for details.

    📚 Thread safety documentation

    🔀 The sync engine is thread-safe: multiple threads can send events to the same state
    machine instance concurrently. This is now documented in the
    processing model and verified by stress tests.
    #592.

    Coroutine functions as invoke targets

    👍 Invoke now supports async def functions and IInvoke handlers with async def run().
    On the async engine, coroutines are awaited directly on the event loop instead of running
    in a thread executor, making invoke a natural fit for non-blocking async I/O
    (e.g., aiohttp, async DB drivers).

    asyncdeffetch\_data():asyncwithaiohttp.ClientSession()assession:resp=awaitsession.get("https://api.example.com/data")returnawaitresp.json()classLoader(StateChart):loading=State(initial=True,invoke=fetch\_data)ready=State(final=True)done\_invoke\_loading=loading.to(ready)
    

    👀 See Coroutine functions for details.
    #611,
    🛠 fixes #610.

    🛠 Bugfixes in 3.1.0

    • 🛠 Fixes silent misuse of Event() with multiple positional arguments. Passing more than one
      transition to Event() (e.g., Event(t1, t2)) now raises InvalidDefinition with a
      clear message suggesting the | operator. Previously, the second argument was silently
      interpreted as the event id, leaving the extra transitions eventless (auto-firing).
      #588.

    • Event.name is now auto-humanized from the id (e.g., cycleCycle,
      pick_upPick up). Diagrams, Mermaid output, and text tables all display
      the human-readable name. Explicit name= values are preserved. The same
      humanize_id() helper is now shared by Event and State.
      #601,
      🛠 fixes #600.

    • 🗄 current_state setter now emits DeprecationWarning consistently with the getter.
      ⚠ Previously only reading current_state triggered the warning; assigning to it was silent.
      🗄 The docstring also now includes a deprecated directive for Sphinx autodoc.
      #604.

    • 🔧 Configuration.add()/discard() now write through the model setter, ensuring state
      🔄 changes are persisted correctly on domain models (e.g., Django models with custom setters).
      ⚡️ Previously the configuration was updated in-place without notifying the model layer.
      #596.

    • States.from_enum() now works correctly inside compound and parallel states. Previously
      the states were silently not collected when used as a nested state declaration.
      #607,
      🛠 fixes #606.

    Misc in 3.1.0

    • 🔨 Internal refactor of Configuration to always normalize to OrderedSet internally, with
      two boundary helpers (_read_from_model / _write_to_model) confining the
      None | scalar | OrderedSet trichotomy to the model edge. Public API is unchanged.
      #599.

    • ⬇️ Reduced allocation overhead in Configuration.add() / discard() by mutating the
      OrderedSet in place and writing back via the setter, removing the ~4–5% overhead
      introduced by the persistence fix in #596.

    • 📦 Diagram module restructured into a package and doctests in docs/diagram.md replaced by
      🔀 the new Sphinx directive; a pre-commit hook now keeps generated diagrams in sync.
      #589,
      #590.

    • ⬆️ Bumped the minimum pydot version to 4.0.1 for the diagrams optional extra, plus a
      ✅ general refresh of dev dependencies (ruff, pytest-cov, pytest-asyncio, Django, furo, etc.).
      #608.

  • v3.0.0 Changes

    February 24, 2026

    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...

  • v2.6.0 Changes

    February 13, 2026

    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.
  • v2.5.0 Changes

    June 04, 2025

    StateMachine 2.5.0

    December 3, 2024

    What's new in 2.5.0

    🚀 This release improves Condition expressions and explicit definition of Events and introduces the helper State.from_.any().

    Python compatibility in 2.5.0

    👍 StateMachine 2.5.0 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, and 3.13.

    Helper to declare transition from any state

    You can now declare that a state is accessible from any other state with a simple constructor. Using State.from_.any(), the state machine meta class automatically creates transitions from all non-final states to the target state.

    🔨 Furthermore, both State.from_.itself() and State.to.itself() have been refactored to support type hints and are now fully visible for code completion in your preferred editor.

    \>\>\>fromstatemachineimportEvent\>\>\>classAccountStateMachine(StateMachine):
    ...active=State("Active",initial=True)
    ...suspended=State("Suspended")
    ...overdrawn=State("Overdrawn")
    ...closed=State("Closed",final=True)
    ...
    ...suspend=Event(active.to(suspended))
    ...activate=Event(suspended.to(active))
    ...overdraft=Event(active.to(overdrawn))
    ...resolve\_overdraft=Event(overdrawn.to(active))
    ...
    ...close\_account=Event(closed.from\_.any(cond="can\_close\_account"))
    ...
    ...can\_close\_account:bool=True...
    ...defon\_close\_account(self):
    ...print("Account has been closed.")\>\>\>sm=AccountStateMachine()\>\>\>sm.close\_account()Accounthasbeenclosed.\>\>\>sm.closed.is\_activeTrue
    

    👍 Allowed events are now bounded to the state machine instance

    Since 2.0, the state machine can return a list of allowed events given the current state:

    \>\>\>sm=AccountStateMachine()\>\>\>[str(e)foreinsm.allowed\_events]
    ['suspend','overdraft','close\_account']
    

    Event instances are now bound to the state machine instance, allowing you to pass the event by reference and call it like a method, which triggers the event in the state machine.

    You can think of the event as an implementation of the command design pattern.

    On this example, we iterate until the state machine reaches a final state,
    listing the current state allowed events and executing the simulated user choice:

    \>\>\>importrandom\>\>\>random.seed("15")\>\>\>sm=AccountStateMachine()\>\>\>whilenotsm.current\_state.final:
    ...allowed\_events=sm.allowed\_events...print("Choose an action: ")
    ...foridx,eventinenumerate(allowed\_events):
    ...print(f"{idx} - {event.name}")
    ...
    ...user\_input=random.randint(0,len(allowed\_events)-1)
    ...print(f"User input: {user\_input}")
    ...
    ...event=allowed\_events[user\_input]
    ...print(f"Running the option {user\_input} - {event.name}")
    ...event()Chooseanaction:0-Suspend1-Overdraft2-CloseaccountUserinput:0Runningtheoption0-SuspendChooseanaction:0-Activate1-CloseaccountUserinput:0Runningtheoption0-ActivateChooseanaction:0-Suspend1-Overdraft2-CloseaccountUserinput:2Runningtheoption2-CloseaccountAccounthasbeenclosed.\>\>\>print(f"SM is in {sm.current\_state.name} state.")SMisinClosedstate.
    

    Conditions expressions in 2.5.0

    🚀 This release adds support for comparison operators into Condition expressions.

    👍 The following comparison operators are supported:

    1. > — Greather than.
    2. >= — Greather than or equal.
    3. == — Equal.
    4. != — Not equal.
    5. < — Lower than.
    6. <= — Lower than or equal.

    Example:

    \>\>\>fromstatemachineimportStateMachine,State,Event\>\>\>classAnyConditionSM(StateMachine):
    ...start=State(initial=True)
    ...end=State(final=True)
    ...
    ...submit=Event(
    ...start.to(end,cond="order\_value \> 100"),
    ...name="finish order",
    ... )
    ...
    ...order\_value:float=0\>\>\>sm=AnyConditionSM()\>\>\>sm.submit()Traceback(mostrecentcalllast):TransitionNotAllowed:Can'tfinishorderwheninStart.\>\>\>sm.order\_value=135.0\>\>\>sm.submit()\>\>\>sm.current\_state.id'end'
    
    See [Condition expressions](https://python-statemachine.readthedocs.io/en/v2.5.0/guards.html#condition-expressions) for more details or take a look at the {ref}`sphx_glr_auto_examples_lor_machine.py` example.
    

    Decorator callbacks with explicit event creation in 2.5.0

    Now you can add callbacks using the decorator syntax using Events. Note that this syntax is also available without the explicit Event.

    \>\>\>fromstatemachineimportStateMachine,State,Event\>\>\>classStartMachine(StateMachine):
    ...created=State(initial=True)
    ...started=State(final=True)
    ...
    ...start=Event(created.to(started),name="Launch the machine")
    ...
    ... @start.on...defcall\_service(self):
    ...return"calling..."...\>\>\>sm=StartMachine()\>\>\>sm.start()'calling...'
    

    🛠 Bugfixes in 2.5.0

    • 🛠 Fixes #500 issue adding support for Pickle.

    Misc in 2.5.0

    • We're now using uv#491.
    • Simplification of the engines code #498.
    • 🔨 The dispatcher and callback modules where refactored with improved separation of concerns #490.
  • v2.4.0 Changes

    November 05, 2024

    StateMachine 2.4.0

    November 5, 2024

    What's new in 2.4.0

    🚀 This release introduces powerful new features for the StateMachine library: {ref}Condition expressions and explicit definition of {ref}Events. These updates make it easier to define complex transition conditions and enhance performance, especially in workflows with nested or recursive event structures.

    Python compatibility in 2.4.0

    👍 StateMachine 2.4.0 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, and 3.13.

    Conditions expressions in 2.4.0

    🚀 This release introduces support for conditionals with Boolean algebra. You can now use expressions like or, and, and not directly within transition conditions, simplifying the definition of complex state transitions. This allows for more flexible and readable condition setups in your state machine configurations.

    Example (with a spoiler of the next highlight):

    \>\>\>fromstatemachineimportStateMachine,State,Event\>\>\>classAnyConditionSM(StateMachine):
    ...start=State(initial=True)
    ...end=State(final=True)
    ...
    ...submit=Event(
    ...start.to(end,cond="used\_money or used\_credit"),
    ...name="finish order",
    ... )
    ...
    ...used\_money:bool=False...used\_credit:bool=False\>\>\>sm=AnyConditionSM()\>\>\>sm.submit()Traceback(mostrecentcalllast):TransitionNotAllowed:Can'tfinishorderwheninStart.\>\>\>sm.used\_credit=True\>\>\>sm.submit()\>\>\>sm.current\_state.id'end'
    
    See {ref}`Condition expressions` for more details or take a look at the {ref}`sphx_glr_auto_examples_lor_machine.py` example.
    

    Explicit event creation in 2.4.0

    🌐 Now you can explicit declare {ref}Events using the {ref}event class. This allows custom naming, translations, and also helps your IDE to know that events are callable.

    \>\>\>fromstatemachineimportStateMachine,State,Event\>\>\>classStartMachine(StateMachine):
    ...created=State(initial=True)
    ...started=State(final=True)
    ...
    ...start=Event(created.to(started),name="Launch the machine")
    ...\>\>\>[e.idforeinStartMachine.events]
    ['start']\>\>\>[e.nameforeinStartMachine.events]
    ['Launch the machine']\>\>\>StartMachine.start.name'Launch the machine'
    
    See {ref}`Events` for more details.
    

    Recursive state machines (infinite loop)

    🚀 We removed a note from the docs saying to avoid recursion loops. Since the {ref}StateMachine 2.0.0 release we've turned the RTC model enabled by default, allowing nested events to occour as all events are put on an internal queue before being executed.

    See {ref}`sphx_glr_auto_examples_recursive_event_machine.py` for an example of an infinite loop state machine declaration using `after` action callback to call the same event over and over again.
    

    🛠 Bugfixes in 2.4.0

    • 🛠 Fixes #484 issue where nested events inside loops could leak memory by incorrectly
      🐎 referencing previous event_data when queuing the next event. This fix improves performance and stability in event-heavy workflows.
  • v2.3.1 Changes

    June 10, 2024

    StateMachine 2.3.1

    June 7, 2024

    What's new in 2.3.1

    🚀 This release has a high expected feature, we're adding asynchronous support, and enhancing overall functionality. In fact, the approach we took was to go all the way down changing the internals of the library to be fully async, keeping only the current external API as a thin sync/async adapter.

    Python compatibility 2.3.1

    👍 StateMachine 2.3.1 supports Python 3.7, 3.8, 3.9, 3.10, 3.11 and 3.12.

    📦 We've fixed a bug on the package declaration that was preventing users from Python 3.7 to install the latest version.

    👍 Asynchronous Support in 2.3.1

    🚀 This release introduces native coroutine support using asyncio, enabling seamless integration with asynchronous code.

    Now you can send and await for events, and also write async Actions, Conditions and Validators.

    \>\>\>classAsyncStateMachine(StateMachine):
    ...initial=State('Initial',initial=True)
    ...final=State('Final',final=True)
    ...
    ...advance=initial.to(final)\>\>\>asyncdefrun\_sm():
    ...sm=AsyncStateMachine()
    ...awaitsm.advance()
    ...print(sm.current\_state)\>\>\>asyncio.run(run\_sm())Final
    
  • v2.2.0 Changes

    May 07, 2024

    StateMachine 2.2.0

    May 6, 2024

    What's new in 2.2.0

    🚀 In this release, we conducted a general cleanup and refactoring across various modules to enhance code readability and maintainability. We improved exception handling and reduced code redundancy.

    As a result, we achieved a ~2.2x faster setup in our performance tests and significantly simplified the callback machinery.

    Check of unreachable and non-final states

    We included one more state machine definition validation for non-final states.

    👻 We already check if any states are unreachable from the initial state, if not, an InvalidDefinition exception is thrown.

    \>\>\>fromstatemachineimportStateMachine,State\>\>\>classTrafficLightMachine(StateMachine):
    ..."A workflow machine"...red=State('Red',initial=True,value=1)
    ...green=State('Green',value=2)
    ...orange=State('Orange',value=3)
    ...hazard=State('Hazard',value=4)
    ...
    ...cycle=red.to(green)|green.to(orange)|orange.to(red)
    ...blink=hazard.to.itself()Traceback(mostrecentcalllast):
    ...InvalidDefinition:Thereareunreachablestates.Thestatemachinegraphshouldhaveasinglecomponent.Disconnectedstates: ['hazard']
    

    🚀 From this release, StateMachine will also check that all non-final states have an outgoing transition,
    and warn you if any states would result in the statemachine becoming trapped in a non-final state with no further transitions possible.

    This will currently issue a warning, but can be turned into an exception by setting `strict_states=True` on the class.
    
    \>\>\>fromstatemachineimportStateMachine,State\>\>\>classTrafficLightMachine(StateMachine,strict\_states=True):
    ..."A workflow machine"...red=State('Red',initial=True,value=1)
    ...green=State('Green',value=2)
    ...orange=State('Orange',value=3)
    ...hazard=State('Hazard',value=4)
    ...
    ...cycle=red.to(green)|green.to(orange)|orange.to(red)
    ...fault=red.to(hazard)|green.to(hazard)|orange.to(hazard)Traceback(mostrecentcalllast):
    ...InvalidDefinition:Allnon-finalstatesshouldhaveatleastoneoutgoingtransition.Thesestateshavenooutgoingtransition: ['hazard']
    
    `strict_states=True` will become the default behavior in the next major release.
    

    👀 See State Transitions.

    🛠 Bugfixes in 2.2.0

    • 🛠 Fixes #424 allowing deepcopy of state machines.
    • Dispatch Mechanism : Resolved issues in the dispatch mechanism in statemachine/dispatcher.py that affected the reliability
      of event handling across different states. This fix ensures consistent behavior when events are dispatched in complex state
      🔧 machine configurations.
  • v2.1.2 Changes

    October 06, 2023

    StateMachine 2.1.2

    October 6, 2023

    🚀 This release improves the setup performance of the library by a 10x factor, with a major
    🔨 refactoring on how we handle the callbacks registry and validations.

    👀 See #401 for the technical details.

    Python compatibility 2.1.2

    👍 StateMachine 2.1.2 supports Python 3.7, 3.8, 3.9, 3.10, 3.11 and 3.12.

    🚀 On the next major release (3.0.0), we will drop support for Python 3.7.

    🛠 Bugfixes in 2.1.2

    • 🛠 Fixes #406 action callback being
      called twice when mixing decorator syntax combined with the naming convention.