NewYour coding agent can read the release notes before it upgrades.Set up the MCP server →
PyPI · #3392 most downloaded on PyPI
Python Finite State Machines made easy.
Last release 2 months ago
01 Aug 2026
Ships unpredictably
gaps range from 1 weeks to 1.2 years
Nearly every release is documented
notes for 35 of 38 stable releases
3 versions withdrawn
withdrawn after publishing
10 years old
41 releases · first in 2017
3.2.1 is a security release. It closes three issues in the secure-by-default IO layer introduced in 3.2.0, all reachable when loading a document with…
3.2.1 is a security release. It closes three issues in the secure-by-default IO layer introduced in 3.2.0, all reachable when loading a document with the default trusted=False, and sharpens the security model: a statechart is an executable document, not inert data. The secure-by-default mode guarantees confidentiality and integrity (loading runs no arbitrary code and reads no arbitrary files); it does not sandbox availability, and it is hardening for your own dynamic definitions, not a sandbox for genuinely adversarial documents. See the IO security guide.
Note
Am I affected?
statemachine.io.load(...) / build_processor(...) / SCXMLProcessor) with the default trusted=False.trusted=True for fully controlled documents.Affected versions: >= 3.2.0, < 3.2.1 (this attack surface shipped with the secure-by-default IO layer in 3.2.0). Fixed in 3.2.1.
src (GHSA-fj3w-533r-fvf6)Loading a document with <data src="file:…"> or <invoke src="…"> (or srcexpr) read the named local file during loading, regardless of trusted. A document from an untrusted source could read an arbitrary file on the host (e.g. file:///etc/passwd) and exfiltrate it through the datamodel. External src references (including relative paths, so a document that includes a child from disk) are now rejected with InvalidDefinition unless trusted=True, the same gate already used for <script>. Separately, the SCXML parser now refuses any <!DOCTYPE>/DTD, neutralizing XML entity-expansion denial-of-service bombs (billion laughs / quadratic blowup) at parse time, independent of trusted.
Reported independently by @Pig-Tail, @the-vibe-dev (GHSA-82pq-c599-953q) and @manus-use (GHSA-v2p7-x2f2-p4xg).
<assign> and friends (GHSA-v3qq-3xvg-m77g, GHSA-4857-ggqc-p3jc)Actions that pick a write destination from the document (<assign location>, <foreach item>/index, <data id>, <send>/<invoke idlocation>) only validated the final attribute. A document could therefore traverse __class__ (e.g. location="__class__.__init__") and setattr on the shared model class, corrupting every state machine in the process, or write to other private/protected attributes. Write targets are now confined to public model attributes on every path segment (private, dunder and engine-protected names are rejected), surfacing as error.execution.
A related route reached the same shared state through the SCXML system-variable views: even though the raw engine names (machine, model, ...) are withheld, the _event and _ioprocessors facades still re-exposed the machine, the interpreter and core State objects through public attribute chains (_ioprocessors.interpreter, _event.trigger_data.machine). Two defenses close it. The facades are now sanitized: they retain no reachable reference to the machine, interpreter or event carriers via public (non-_) attributes, so a restricted expression can no longer walk _event/_ioprocessors back to the engine. And the write-target guard now additionally rejects a live engine-capability instance (the machine, interpreter, State/Transition/Event, and the system-variable facades) as a traversed hop or write target, so even a leaked alias cannot be pivoted onto shared state.
Reported by @manus-use (GHSA-v3qq-3xvg-m77g) and @the-vibe-dev (GHSA-4857-ggqc-p3jc).
The restricted evaluator allowed ** and * with no magnitude bound, so a tiny expression could exhaust CPU (9 ** 9 ** 9, a ~370-million-digit bignum) or memory ([0] * 20000000). Both operators are now magnitude-capped: the denial-of-service forms are rejected before they run, while ordinary scalar arithmetic (x * 2, x ** 2) is unaffected. (trusted=True uses full Python, without these caps.)
Reported by @manus-use.
Note
Availability is still not sandboxed. A running machine's logic (eventless loops, large <foreach>, recursive <invoke>, delayed events) can consume CPU, memory or threads without bound. Run documents from parties you do not trust under your own timeout and OS/process resource limits, or under OS-level isolation. See the IO security guide.
state for on_exit_state across compound boundariesWhen exiting a compound state directly (a transition like child -> outsider), the generic on_exit_state() callback reported the transition's source for every exited state. Exiting child and its parent parent both arrived with state and source bound to child, so the parent level was never observable and the two exit calls were indistinguishable.
This was asymmetric with on_enter_state(), which already binds state (and target) to each individual state being entered. The exit side now matches: state (and source) is bound to the individual state being exited.
>>> from statemachine import State, StateChart
>>> class FSM(StateChart):
... orphan = State(initial=True)
...
... class parent(State.Compound):
... child = State()
...
... switch = orphan.to(parent.child) | parent.child.to(orphan)
...
... def on_exit_state(self, source, state):
... print(f"exit {state.id} (source={source.id})")
>>> sm = FSM()
>>> sm.send("switch")
exit orphan (source=orphan)
>>> sm.send("switch")
exit child (source=child)
exit parent (source=parent)Before this fix, the last line read exit child (source=child), hiding the parent. State-specific callbacks (on_exit_<state>) were already correctly keyed per state and are unaffected. Flat (non-compound) machines are also unaffected, since there the exited state is always the transition's source.
#634.
OrderedSet.__getitem__OrderedSet.__getitem__ raised ValueError (leaking from itertools.islice) when called with a negative index, instead of following the sequence protocol. Negative indices now count from the end like any Python sequence, and an index that is still out of range after normalisation raises IndexError:
>>> from statemachine.orderedset import OrderedSet
>>> s = OrderedSet([1, 2, 3])
>>> s[-1]
3
>>> s[-3]
1
>>> s[-4]
Traceback (most recent call last):
...
IndexError: index -4 out of range#633.
nameA State (or Event) name can now be any object castable to str, including lazy translation proxies (e.g. django.utils.translation.gettext_lazy). Earlier 3.x releases stored the value as-is and later assumed it was a real str, so a proxy broke message formatting (notably the TransitionNotAllowed message) and str(state). The proxy is now kept untouched and only resolved via str() at the point of display, so the active locale is honored at render time instead of at class-definition time.
>>> from statemachine import State, StateMachine
>>> class Lazy: # stand-in for a translation proxy (resolved on str())
... def __init__(self, value):
... self.value = value
... def __str__(self):
... return self.value
>>> class SM(StateMachine):
... draft = State(Lazy("Rascunho"), initial=True)
... published = State(Lazy("Publicado"), final=True)
... publish = draft.to(published)
>>> str(SM.draft)
'Rascunho'#632.
cond expressionsBoolean expressions used in cond / unless had a fast-path that returned the whole string as a single variable name when it looked operator-free. The check was too naive: it only looked for !, a literal space and In(, so any other operator written without surrounding whitespace was swallowed into a variable name. cond="is_paid^is_shipped" and cond="items>0" were resolved as variables literally named is_paid^is_shipped and items>0, failing with InvalidDefinition: Did not found name ... when the machine was built.
The fast-path now triggers only for a lone Python identifier, so every other expression goes through the parser and whitespace is never required:
>>> from statemachine import State, StateMachine
>>> class Order(StateMachine):
... waiting = State(initial=True)
... completed = State(final=True)
...
... complete = waiting.to(completed, cond="is_paid^items>0")
...
... is_paid: bool = False
... items: int = 2
>>> sm = Order()
>>> sm.send("complete")
Traceback (most recent call last):
...
statemachine.exceptions.TransitionNotAllowed: Can't complete when in Waiting.
>>> sm.is_paid = True
>>> sm.send("complete")
>>> sm.completed.is_active
TrueGuards that are a single name (including a bare v) keep taking the fast-path, and != keeps parsing as a comparison rather than a negation.
As a side effect, a cond with a structure that is not valid in a boolean expression, such as cond="user.age", now raises InvalidDefinition: Failed to parse boolean expression 'user.age' instead of reporting the whole string as a name that was not found.
#639.
One column per quarter.
…vulnerability in the old SCXML loader ( CVE-2026-47103 ); see Security below.
Load statecharts from documents. A single, secure statemachine.io.load
reads SCXML, JSON and YAML into a running StateChart. From an inline definition:
from statemachine.io import load
Light = load(
"""
states:
green: {initial: true, on: {next: [{target: red}]}}
red: {on: {next: [{target: green}]}}
""",
format="yaml",
)
sm = Light()
sm.send("next")Or from a file, with the format detected from the extension:
Machine = load("traffic_light.scxml")Safe by default. Expressions in loaded documents are evaluated by a restricted
allowlist, never eval — this also closes a code-execution vulnerability in the old
SCXML loader (CVE-2026-47103); see Security below.
Python 3.10+ now required. Support for the end-of-life Python 3.9 was dropped.
In short: before 3.2.0, loading an SCXML document with SCXMLProcessor evaluated the
expressions inside it with Python's eval/exec, so a .scxml file from an untrusted
source could run arbitrary code on your machine. 3.2.0 makes loading safe by default:
expressions are evaluated by a restricted allowlist and <script> is rejected.
Note
Am I affected?
.scxml documents you did not author, throughSCXMLProcessor (e.g. SCXMLProcessor().parse_scxml(...) or parse_scxml_file(...))io.load() did not exist yet).StateMachine / StateChart), or only.scxml files you wrote yourself. Defining a machine in code never evaluates aAffected versions: only the 3.x line before 3.2 — >= 3.0.0, < 3.2.0. SCXML file
loading was added in 3.0.0 (experimental and undocumented); 2.x and earlier have no SCXML
loader and are not affected. Fixed in 3.2.0.
What changed. Guards and datamodel expressions (cond, <assign>, <send>,
<foreach>, <log>, …) are now compiled by a restricted AST allowlist — arithmetic,
comparisons, collections, indexing, attribute reads and the In(...) predicate, but no
builtins, dunder access, or function/method calls. <script> is rejected. This mirrors
yaml.safe_load vs yaml.load.
Trusting a document. For SCXML you author yourself (hand-written documents or the W3C
conformance suite), opt back into full Python with trusted=True:
from statemachine.io.scxml.processor import SCXMLProcessor
SCXMLProcessor() # safe default: restricted evaluator, <script> rejected
SCXMLProcessor(trusted=True) # full eval/exec — only for documents you trustA restricted-mode document that uses an unsupported construct fails to load with
InvalidDefinition; runtime evaluation errors still surface as error.execution events.
Refs: GHSA-v4jc-pm6r-3vj8, CVE-2026-47103, CWE-95.
Statecharts can now be loaded from declarative documents through a single, secure
facade, statemachine.io.load:
from statemachine.io import load
Machine = load("traffic_light.yaml") # format detected from the extension
sm = Machine().scxml/.xml), JSON (.json) and YAML.yaml/.yml). The format is detected from the file extension or set explicitly withformat=.<script> / arbitrary Python is rejected unless you passtrusted=True. YAML is always parsed with safe-load semantics.cond/unless arecount >= 3, boolean algebra, In(state)); there is a structuredassign/raise/log/if/foreach/send/cancel); the system_event/_sessionid/_name/_ioprocessors) are available in every format;invoke works natively (invoke: [{src|content, id, params, namelist, finalize}]).trusted=False.validate=True (with the [validation] extra) to validate onstatemachine.io.build_processor for documents thatSee the IO and formats guide for the full guide.
The IO stack is a ports-and-adapters design. SCXML defines the execution model and
behavior; the XML syntax is just one format. So the runtime is the format-neutral
Interpreter (statemachine.io.interpreter), parameterized by a reader (the format
port) and an evaluator (secure by default), composing a DefinitionBuilder
(statemachine.io.builder) that compiles the neutral IR (statemachine.io.model) into a
StateChart class. The class registry (for invoke), sessions and the system variables live
in this neutral runtime. SCXMLProcessor is now a thin Interpreter preconfigured with
the SCXML reader; its parse_scxml API is preserved (use io.load for files).
python-statemachine[yaml] # PyYAML, for the YAML format
python-statemachine[validation] # jsonschema, for validate=True
python-statemachine[io] # both of the aboveWarning
Python 3.9 support dropped. StateMachine 3.2.0 requires Python 3.10 or
later. If you cannot upgrade Python yet, pin to python-statemachine<3.2
(the 3.1.x series remains the last line supporting 3.9).
Python 3.9 reached end-of-life on 2025-10-31 and is no longer supported by
the Python core team. StateMachine 3.2 now requires Python 3.10+.
Rationale:
python-statemachine in the 180 days prior to this release;match/case (PEP 634), PEP 604X | Y), PEP 585 built-in generics (list[int] insteadList[int]), and zip(strict=True) (PEP 618) internally.python-statemachine<3.2 to stay on the 3.1.x line.No public API was changed by this drop. Code that runs on 3.10+ today
will continue to run unchanged on 3.2.
statemachine.io.scxml internals reorganizedThe experimental statemachine.io.scxml internals were promoted into a format-neutral
IO core (see What's new above). Code that imported implementation modules directly must
update its imports:
| Before (3.1.x) | After (3.2.0) |
|---|---|
statemachine.io.scxml.schema |
statemachine.io.model |
statemachine.io.scxml.parser |
statemachine.io.scxml.reader |
generic helpers in statemachine.io.scxml.actions |
statemachine.io.actions |
protected_attrs / _eval in statemachine.io.scxml.actions |
statemachine.io.evaluators |
EventDataWrapper etc. in statemachine.io.scxml.actions |
statemachine.io.system_variables |
SCXMLInvoker in statemachine.io.scxml.invoke |
Invoker in statemachine.io.invoke |
statemachine.io.scxml.processor.SCXMLProcessor (now a thin wrapper over the new
format-neutral runtime, minus the removed parse_scxml_file; use io.load for files) and
statemachine.io.create_machine_class_from_definition keep their behavior. The runtime
itself moved into the new, SCXML-agnostic statemachine.io.interpreter.Interpreter and
statemachine.io.builder (see Architecture above).
A StateChart with sibling compound states whose children reuse the same local
id (e.g. each region declares an a and a b) but carry distinct value=
identifiers would collapse in the internal instance-state map, which was keyed by
state.id. The duplicate ids overwrote each other, so dispatching an event
resolved to the wrong State instance and raised TransitionNotAllowed.
Instance states are now keyed by state.value (globally unique, the canonical
identifier already used by states_map), fixing dispatch for nested and parallel
configurations that repeat child names.
invoke no longer cancels itself on its own done.invokeWhen an async invoke completed and its done.invoke event triggered a
transition out of the owning state, the cancel-on-exit path could cancel the
invocation's own task while it was still running. The CancelledError surfaced at
the next await, so the target state's on_enter callback never finished.
The engine now skips self-cancellation when the invocation's task is the currently
running task, letting the originating handler complete normally.
#627.
validators in a dict/JSON transition definition are no longer droppedcreate_machine_class_from_definition (the dict/JSON adapter behind
statemachine.io.load) threaded cond, unless, on, before and
after from each transition definition into the Transition, but not
validators. A validators entry was silently ignored, so it never ran at
send(), leaving dict/JSON-defined machines without the explicit-rejection
channel (raise to abort with a reason) that validators provides. The
TransitionDict type also mistyped validators as bool.
validators is now materialized onto the Transition like the other callback
specs, and the type was corrected to the same callback-spec union as
cond/unless.
pytest >=9.0.3 (CVE-2025, tmpdir handling) for py>=3.10
pydot is optional againStarting 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{statechart:FORMAT} placeholder, skipping the formatter import forstatemachine.contrib.diagram no longer re-exports renderer classes atdot/svgpydot.If your code imports DotRenderer, DotRendererConfig, MermaidRenderer
or MermaidRendererConfig directly from statemachine.contrib.diagram,
import them from the renderer submodule instead:
from statemachine.contrib.diagram.renderers.dot import DotRenderer
from statemachine.contrib.diagram.renderers.mermaid import MermaidRenderer#622.
Picked up from the dependency refresh applied to main:
>=0.15.13>=9.0.3 (CVE-2025, tmpdir handling) for py>=3.10>=5.2.14 for py3.10/3.11 (multiple CVEs)>=12.2.0 for py>=3.10 (multiple CVEs)2.6.3 -> 2.7.0 (GHSA-qccp-gfcp-xxvc), requests2.33.1 -> 2.34.2CI 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.
Thread-safety hardening of the configuration cache
May 15, 2026
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.
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.
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.
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.
current_state setter now emits DeprecationWarning consistently with the getter. Previously only reading current_state triggered the warning; assigning…
May 15, 2026
📘 Full docs: https://python-statemachine.readthedocs.io/en/v3.1.0/
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.
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:
from statemachine.contrib.diagram import formatter
formatter.render(sm, "mermaid")
formatter.supported_formats()
@formatter.register_format("custom")
def _render_custom(machine_or_class):
...See Using the formatter API for details.
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"{sm:mermaid}"python -m statemachine.contrib.diagram MyMachine - --format mermaid:format: mermaid renders via sphinxcontrib-mermaid.See Mermaid format for details.
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:
class TrafficLight(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.
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.
--events and --format optionsThe python -m statemachine.contrib.diagram command now accepts:
--events to instantiate the machine and send events before rendering,--format to choose the output format (mermaid, md, rst, dot, svg,- as the output path to write textSee Command line for details.
#593.
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.
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.
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).
async def fetch_data():
async with aiohttp.ClientSession() as session:
resp = await session.get("https://api.example.com/data")
return await resp.json()
class Loader(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.
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., cycle → Cycle,
pick_up → Pick 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.
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.
Backward incompatible changes in 3.0
Upgrading from 2.x? See upgrade guide for a step-by-step
migration guide.
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 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"}
# TrueEntering 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 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
# TrueEvents in one region don't affect others. See parallel states for full details.
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
# TrueSee history states for full details on shallow vs deep history.
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"}
# TrueThe entire eventless chain cascades in a single macrostep. See eventless
for full details.
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 convention for details.
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
# TruePassing 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 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.
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_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 callbackSee 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.
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): If True, the event is placed in the internal queue and processed in theExisting calls to send() are fully backward compatible.
error.executionWhen 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}
# TrueErrors 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 error execution
for full details.
configuration and configuration_valuesDue 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 configuration.
is_terminated propertyA 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
# TrueIn(state) condition checksConditions 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
# Trueprepare_event() callbackThe 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 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!'StateChart base classThe 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.
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.
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.
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 transition.
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 observers for full documentation.
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.
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
# TrueSee timeout for full documentation.
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
# TrueWhen 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 for details.
Fixes #509.
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.
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.
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 havevalidate_disconnected_states: all states must be reachable from the initial state.See validations for details.
The following SCXML features are not yet implemented and are deferred to a future release:
#_internal, #_parent, and#_<invokeid> send targets are supported)For a step-by-step migration guide with before/after examples, see
Upgrading from 2.x to 3.0.
This section summarizes the breaking changes. For detailed before/after examples and
migration instructions, see the upgrade guide.
rtc parameter (deprecated since 2.3.2) has been removed.current_state deprecated. Use configuration / configuration_values instead.StateChart, states are exited before on callbacksprevious_configuration andnew_configuration — are available in on callbacks. Use atomic_configuration_update=TrueStateMachine class to restore the 2.x behavior.StateChart, self-transitions now trigger on_enter_* /on_exit_* callbacks. Set enable_self_transition_entries = False to restore the old behavior.add_observer() removed. Use add_listener() instead.TransitionNotAllowed changes. Now stores configuration (a set) instead of state,event can be None.allow_event_without_transition moved to class level. No longer an __init__ parameter.States.from_enum default changed. use_enum_instance now defaults to True.get_machine_cls().strict_states removed. Replaced by validate_trap_states andvalidate_final_reachability (both default to True).__repr__ output changed. Now shows configuration=[...] instead of current_state=....This release adds the StateMachine.enabled_events method, Python 3.14 support, a significant performance improvement for callback dispatch, and severa
February 2026
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.
StateMachine 2.6.0 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, and 3.14.
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.
>>> class ApprovalMachine(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.id for e in sm.allowed_events]
['approve', 'reject']
>>> [e.id for e in sm.enabled_events()]
['reject']
>>> sm.is_manager = True
>>> [e.id for e in sm.enabled_events()]
['approve', 'reject']Since conditions may depend on runtime arguments, any *args/**kwargs passed to
enabled_events() are forwarded to the condition callbacks:
>>> class TaskMachine(StateMachine):
... idle = State(initial=True)
... running = State(final=True)
...
... start = idle.to(running, cond="has_enough_resources")
...
... def has_enough_resources(self, cpu=0):
... return cpu >= 4
>>> sm = TaskMachine()
>>> sm.enabled_events()
[]
>>> [e.id for e in sm.enabled_events(cpu=8)]
['start']See Checking enabled events in the Guards documentation for more details.
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.
__bool__ was being replaced by the default Model().not, and, or) were not being awaited, causing guards toVAR_POSITIONAL and kwargs precedence bugs in the signature binding cache introducedTransitionList to Event.MachineMixinValueError.This release improves Condition expressions and explicit definition of Events and introduces the helper State.from_.any() .
December 3, 2024
This release improves Condition expressions and explicit definition of Events and introduces the helper State.from_.any().
StateMachine 2.5.0 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, and 3.13.
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.
>>> from statemachine import Event
>>> class AccountStateMachine(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
...
... def on_close_account(self):
... print("Account has been closed.")
>>> sm = AccountStateMachine()
>>> sm.close_account()
Account has been closed.
>>> sm.closed.is_active
TrueSince 2.0, the state machine can return a list of allowed events given the current state:
>>> sm = AccountStateMachine()
>>> [str(e) for e in sm.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:
>>> import random
>>> random.seed("15")
>>> sm = AccountStateMachine()
>>> while not sm.current_state.final:
... allowed_events = sm.allowed_events
... print("Choose an action: ")
... for idx, event in enumerate(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()
Choose an action:
0 - Suspend
1 - Overdraft
2 - Close account
User input: 0
Running the option 0 - Suspend
Choose an action:
0 - Activate
1 - Close account
User input: 0
Running the option 0 - Activate
Choose an action:
0 - Suspend
1 - Overdraft
2 - Close account
User input: 2
Running the option 2 - Close account
Account has been closed.
>>> print(f"SM is in {sm.current_state.name} state.")
SM is in Closed state.This release adds support for comparison operators into Condition expressions.
The following comparison operators are supported:
> — Greather than.>= — Greather than or equal.== — Equal.!= — Not equal.< — Lower than.<= — Lower than or equal.Example:
>>> from statemachine import StateMachine, State, Event
>>> class AnyConditionSM(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 (most recent call last):
TransitionNotAllowed: Can't finish order when in Start.
>>> 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.
Now you can add callbacks using the decorator syntax using Events. Note that this syntax is also available without the explicit Event.
>>> from statemachine import StateMachine, State, Event
>>> class StartMachine(StateMachine):
... created = State(initial=True)
... started = State(final=True)
...
... start = Event(created.to(started), name="Launch the machine")
...
... @start.on
... def call_service(self):
... return "calling..."
...
>>> sm = StartMachine()
>>> sm.start()
'calling...'
December 3, 2024
This release improves {ref}Condition expressions and explicit definition of {ref}Events and introduces the helper State.from_.any().
StateMachine 2.5.0 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, and 3.13.
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.
>>> from statemachine import Event
>>> class AccountStateMachine(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
...
... def on_close_account(self):
... print("Account has been closed.")
>>> sm = AccountStateMachine()
>>> sm.close_account()
Account has been closed.
>>> sm.closed.is_active
True
Since 2.0, the state machine can return a list of allowed events given the current state:
>>> sm = AccountStateMachine()
>>> [str(e) for e in sm.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:
import random
random.seed("15")
sm = AccountStateMachine()
while not sm.current_state.final:
allowed_events = sm.allowed_events
print("Choose an action: ")
for idx, event in enumerate(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()
print(f"SM is in {sm.current_state.name} state.")
# SM is in Closed state.
This release adds support for comparison operators into {ref}Condition expressions.
The following comparison operators are supported:
> — Greather than.>= — Greather than or equal.== — Equal.!= — Not equal.< — Lower than.<= — Lower than or equal.Example:
from statemachine import StateMachine, State, Event
class AnyConditionSM(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()
# TransitionNotAllowed: Can't finish order when in Start.
sm.order_value = 135.0
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.
Now you can add callbacks using the decorator syntax using {ref}Events. Note that this syntax is also available without the explicit Event.
>>> from statemachine import StateMachine, State, Event
>>> class StartMachine(StateMachine):
... created = State(initial=True)
... started = State(final=True)
...
... start = Event(created.to(started), name="Launch the machine")
...
... @start.on
... def call_service(self):
... return "calling..."
...
>>> sm = StartMachine()
>>> sm.start()
'calling...'
This release introduces powerful new features for the StateMachine library: {ref}Condition expressions and explicit definition of {ref}Events. These u
November 5, 2024
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.
StateMachine 2.4.0 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, and 3.13.
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):
>>> from statemachine import StateMachine, State, Event
>>> class AnyConditionSM(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 (most recent call last):
TransitionNotAllowed: Can't finish order when in Start.
>>> 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.
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.
>>> from statemachine import StateMachine, State, Event
>>> class StartMachine(StateMachine):
... created = State(initial=True)
... started = State(final=True)
...
... start = Event(created.to(started), name="Launch the machine")
...
>>> [e.id for e in StartMachine.events]
['start']
>>> [e.name for e in StartMachine.events]
['Launch the machine']
>>> StartMachine.start.name
'Launch the machine'
See {ref}`Events` for more details.
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.
event_data when queuing the next event. This fix improves performance and stability in event-heavy workflows.Fixes #474 install with extra was not working to install pydot.
Added Python 3.13 on the test matrix. StateMachine 2.3.5 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12 and 3.13.
Fixes #465 regression that caused exception when registering a listener with unbounded callbacks.
July 11, 2024
Deprecations that will be removed on the next major release:
July 3, 2024
Deprecations that will be removed on the next major release:
States.from_enum(..., use_enum_instance=True) will be the default.See {ref}`States from Enum types` for more details.
Deprecations that will be removed on the next major release:
July 01, 2024
Observers are now rebranded to {ref}listeners. With expanted support for adding listeners when
instantiating a state machine. This allows covering more use cases. We also improved the async support.
Since version 2.3.0, we have added async support. However, we encountered use cases, such as the async safety on Django ORM, which expects no running event loop and blocks if it detects one on the current thread.
To address this issue, we developed a solution that maintains a unified API for both synchronous and asynchronous operations while effectively handling these scenarios.
This is achieved through a new concept called "engine," an internal strategy pattern abstraction that manages transitions and callbacks.
There are two engines:
SyncEngine : Activated if there are no async callbacks. All code runs exactly as it did before version 2.3.0.
AsyncEngine : Activated if there is at least one async callback. The code runs asynchronously and requires a running event loop, which it will create if none exists.
These engines are internal and are activated automatically by inspecting the registered callbacks in the following scenarios:
See {ref}`async` for more details.
Listeners are a way to generically add behavior to a state machine without changing its internal implementation.
Example:
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
>>> class LogListener(object):
... def __init__(self, name):
... self.name = name
...
... def after_transition(self, event, source, target):
... print(f"{self.name} after: {source.id}--({event})-->{target.id}")
...
... def on_enter_state(self, target, event):
... print(f"{self.name} enter: {target.id} from {event}")
>>> sm = TrafficLightMachine(listeners=[LogListener("Paulista Avenue")])
Paulista Avenue enter: green from __initial__
>>> sm.cycle()
Running cycle from green to yellow
Paulista Avenue enter: yellow from cycle
Paulista Avenue after: green--(cycle)-->yellow
See {ref}`listeners` for more details.
Now it's possible to bind events to external objets. One expected use case is in conjunction with the {ref}Mixins models,
that wrap state machines internally. This way you don't need to expose the state machine.
See {ref}`sphx_glr_auto_examples_user_machine.py` for an example binding event triggers with a state machine.
Deprecations that will be removed on the next major release:
StateMachine.add_observer is deprecated in favor of StateMachine.add_listener.StateMachine.rtc option is deprecated. We'll keep only the run-to-completion (RTC) model.This release has a high expected feature, we're adding asynchronous support, and enhancing overall functionality. In fact, the approach we took was to
June 7, 2024
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.
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.
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.
>>> class AsyncStateMachine(StateMachine):
... initial = State('Initial', initial=True)
... final = State('Final', final=True)
...
... advance = initial.to(final)
>>> async def run_sm():
... sm = AsyncStateMachine()
... await sm.advance()
... print(sm.current_state)
>>> asyncio.run(run_sm())
Final
This release has a high expected feature, we're adding asynchronous support, and enhancing overall functionality. In fact, the approach we took was to
June 7, 2024
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.
StateMachine 2.3.0 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.
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 {ref}Actions, {ref}Conditions and {ref}Validators.
See {ref}`sphx_glr_auto_examples_air_conditioner_machine.py` for an example of
async code with a state machine.
class AsyncStateMachine(StateMachine):
initial = State("Initial", initial=True)
final = State("Final", final=True)
advance = initial.to(final)
async def on_advance(self):
return 42
async def run_sm():
sm = AsyncStateMachine()
res = await sm.advance()
return (42, sm.current_state.name)
asyncio.run(run_sm())
# (42, 'Final')
In this release, we conducted a general cleanup and refactoring across various modules to enhance code readability and maintainability. We improved ex
May 6, 2024
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.
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.
>>> from statemachine import StateMachine, State
>>> class TrafficLightMachine(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 (most recent call last):
...
InvalidDefinition: There are unreachable states. The statemachine graph should have a single component. Disconnected states: ['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.
>>> from statemachine import StateMachine, State
>>> class TrafficLightMachine(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 (most recent call last):
...
InvalidDefinition: All non-final states should have at least one outgoing transition. These states have no outgoing transition: ['hazard']
`strict_states=True` will become the default behavior in the next major release.
See State Transitions.
deepcopy of state machines.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.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 valid
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.
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.
Fixes #391 adding support to pytest-mock spy method.
August 3, 2023
spy method.Given an Enum type that declares our expected states:
June 11, 2023
Given an Enum type that declares our expected states:
>>> from enum import Enum
>>> class Status(Enum):
... pending = 1
... completed = 2
A StateMachine can be declared as follows:
>>> from statemachine import StateMachine
>>> from statemachine.states import States
>>> class ApprovalMachine(StateMachine):
...
... _ = States.from_enum(Status, initial=Status.pending, final=Status.completed)
...
... finish = _.pending.to(_.completed)
...
... def on_enter_completed(self):
... print("Completed!")
These release notes cover the what's new in 2.0, as well as some backward incompatible changes you'll want to be aware of when upgrading from StateMac…
March 5, 2023
Welcome to StateMachine 2.0.0!
This version is the first to take advantage of the Python3 improvements and is a huge internal refactoring removing the deprecated features on 1.*. We hope that you enjoy it.
These release notes cover the what's new in 2.0, as well as some backward incompatible changes you'll
want to be aware of when upgrading from StateMachine 1.*.
StateMachine 2.0 supports Python 3.7, 3.8, 3.9, 3.10, and 3.11.
There are now two distinct methods for processing events in the library. The new default is to run in RTC model to be compliant with the specs, where the event is put on a queue before processing. You can also configure your state machine to run back in Non-RTC model, where the event will be run immediately and nested events will be chained.
This means that the state machine now completes all the actions associated with an event before moving on to the next event. Even if you trigger an event inside an action.
See processing model for more details.
State names are now by default derived from the class variable that they are assigned to. You can keep declaring explicit names, but we encourage you to only assign a name when it is different than the one derived from its id.
>>> from statemachine import StateMachine, State
>>> class ApprovalMachine(StateMachine):
... pending = State(initial=True)
... waiting_approval = State()
... approved = State(final=True)
...
... start = pending.to(waiting_approval)
... approve = waiting_approval.to(approved)
...
>>> ApprovalMachine.pending.name
'Pending'
>>> ApprovalMachine.waiting_approval.name
'Waiting approval'
>>> ApprovalMachine.approved.name
'Approved'
An internal transition is like a self transition, but in contrast, no entry or exit actions are ever executed as a result of an internal transition.
>>> from statemachine import StateMachine, State
>>> class TestStateMachine(StateMachine):
... initial = State(initial=True)
...
... loop = initial.to.itself(internal=True)
See internal transition for more details.
You can now instantiate a StateMachine with allow_event_without_transition=True,
so the state machine will allow triggering events that may not lead to a state transition,
including tolerance to unknown event triggers.
The default value is False, that keeps the backward compatible behavior of when an
event does not result in a transition, an exception TransitionNotAllowed will be raised.
>>> sm = ApprovalMachine(allow_event_without_transition=True)
>>> sm.send("unknow_event_name")
>>> sm.pending.is_active
True
>>> sm.send("approve")
>>> sm.pending.is_active
True
>>> sm.send("start")
>>> sm.waiting_approval.is_active
True
Now the library messages can be translated into any language.
See Add a translation on how to contribute with translations.
Transition
guards using decorators is now possible.StateMachine to add behavior on your own base class. Abstract StateMachine cannot be instantiated.1.6 for auto-discovering and registering StateMachine classes
to be used on django integration.While we've figured out a way to keep near complete backwards compatible changes to the new Run to completion (RTC) by default feature (all built-in examples run without change), if you encounter problems when upgrading to this version, you can still switch back to the old Non-RTC model. Be aware that we may remove the Non-RTC model in the future.
StateMachine.run removed in favor of StateMachine.sendfrom tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
sm.run("cycle")
Should become:
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
>>> sm = TrafficLightMachine()
>>> sm.send("cycle")
'Running cycle from green to yellow'
StateMachine.allowed_transitions removed in favor of StateMachine.allowed_eventsfrom tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
assert [t.name for t in sm.allowed_transitions] == ["cycle"]
Should become:
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
>>> sm = TrafficLightMachine()
>>> assert [t.name for t in sm.allowed_events] == ["cycle", "slowdown"]
Statemachine.is_<state> removed in favor of StateMachine.<state>.is_activefrom tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
assert sm.is_green
Should become:
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
>>> sm = TrafficLightMachine()
>>> assert sm.green.is_active
State.identification removed in favor of State.idfrom tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
assert sm.current_state.identification == "green"
Should become:
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
>>> sm = TrafficLightMachine()
>>> assert sm.current_state.id == "green"
March 5, 2023
Welcome to StateMachine 2.0.0!
This version is the first to take advantage of the Python3 improvements and is a huge internal refactoring removing the deprecated features on 1.*. We hope that you enjoy it.
These release notes cover the , as well as some backward incompatible changes you'll want to be aware of when upgrading from StateMachine 1.*.
StateMachine 2.0 supports Python 3.7, 3.8, 3.9, 3.10, and 3.11.
There are now two distinct methods for processing events in the library. The new default is to run in
{ref}RTC model to be compliant with the specs, where the {ref}event is put on a queue before processing.
You can also configure your state machine to run back in {ref}Non-RTC model, where the {ref}event will
be run immediately and nested events will be chained.
This means that the state machine now completes all the actions associated with an event before moving on to the next event. Even if you trigger an event inside an action.
See {ref}`processing model` for more details.
{ref}State names are now by default derived from the class variable that they are assigned to.
You can keep declaring explicit names, but we encourage you to only assign a name
when it is different than the one derived from its id.
>>> from statemachine import StateMachine, State
>>> class ApprovalMachine(StateMachine):
... pending = State(initial=True)
... waiting_approval = State()
... approved = State(final=True)
...
... start = pending.to(waiting_approval)
... approve = waiting_approval.to(approved)
...
>>> ApprovalMachine.pending.name
'Pending'
>>> ApprovalMachine.waiting_approval.name
'Waiting approval'
>>> ApprovalMachine.approved.name
'Approved'
An internal transition is like a {ref}self transition, but in contrast, no entry or exit actions
are ever executed as a result of an internal transition.
>>> from statemachine import StateMachine, State
>>> class TestStateMachine(StateMachine):
... initial = State(initial=True)
...
... loop = initial.to.itself(internal=True)
See {ref}`internal transition` for more details.
You can now instantiate a {ref}StateMachine with allow_event_without_transition=True,
so the state machine will allow triggering events that may not lead to a state {ref}transition,
including tolerance to unknown {ref}event triggers.
The default value is False, that keeps the backward compatible behavior of when an
event does not result in a {ref}transition, an exception TransitionNotAllowed will be raised.
>>> import pytest
>>> pytest.skip("Since 3.0.0 `allow_event_without_transition` is now a class attribute.")
>>> sm = ApprovalMachine(allow_event_without_transition=True)
>>> sm.send("unknow_event_name")
>>> sm.pending.is_active
True
>>> sm.send("approve")
>>> sm.pending.is_active
True
>>> sm.send("start")
>>> sm.waiting_approval.is_active
True
Now the library messages can be translated into any language.
See {ref}Add a translation on how to contribute with translations.
Transition
guards using decorators is now possible.diagrams for more details.StateMachine to add behavior on your own base class. Abstract StateMachine cannot be instantiated.1.6 for auto-discovering and registering StateMachine classes
to be used on {ref}django integration.Prior to #365, when you {ref}Declare transition actions by naming convention, all callbacks of the transition were called even if the triggered event was not the one that originated the transition.
This behavior was fixed in this release. Now, only the transitions associated with the triggered event or directly assigned to the transition are called.
Consider the following state machine as an example:
>>> from statemachine import State
>>> from statemachine import StateMachine
>>> class TrafficLightMachine(StateMachine):
... "A traffic light machine"
... green = State(initial=True)
... yellow = State()
... red = State()
...
... slowdown = green.to(yellow)
... stop = yellow.to(red)
... go = red.to(green)
...
... cycle = slowdown | stop | go
...
... def before_slowdown(self):
... print("Slowdown")
...
... def before_cycle(self, event: str, source: State, target: State):
... print(f"Running {event} from {source.id} to {target.id}")
Before, if you send the cycle event, the behavior was to also trigger actions associated with
slowdown, because they're sharing the same instance of {ref}Transition:
>>> sm = TrafficLightMachine()
>>> sm.send("cycle") # doctest: +SKIP
Slowdown
Running cycle from green to yellow
Now the behavior is to only execute actions bound to the triggered {ref}event or directly
associated to the {ref}Transition:
>>> sm = TrafficLightMachine()
>>> sm.send("cycle")
Running cycle from green to yellow
If you want to emulate the previous behavior, consider one of these alternatives.
You can {ref}Bind transition actions using params or {ref}Bind transition actions using decorator syntax:
>>> from statemachine import State
>>> from statemachine import StateMachine
>>> class TrafficLightMachine(StateMachine):
... "A traffic light machine"
... green = State(initial=True)
... yellow = State()
... red = State()
...
... slowdown = green.to(yellow, before="do_before_slowdown") # assign to the transition
... stop = yellow.to(red)
... go = red.to(green)
...
... cycle = slowdown | stop | go
...
... def do_before_slowdown(self):
... print("Slowdown")
...
... @stop.before # assign to the transition
... def do_before_stop(self):
... print("Stop")
...
... def before_cycle(self, event: str, source: State, target: State):
... print(f"Running {event} from {source.id} to {target.id}")
You can go an step further and if the events are not called externally, get rid of them and put the actions directly on the transitions:
>>> from statemachine import State
>>> from statemachine import StateMachine
>>> class TrafficLightMachine(StateMachine):
... "A traffic light machine"
... green = State(initial=True)
... yellow = State()
... red = State()
...
... cycle = (
... green.to(yellow, before="slowdown")
... | yellow.to(red, before="stop")
... | red.to(green, before="go")
... )
...
... def slowdown(self):
... print("Slowdown")
...
... def stop(self):
... print("Stop")
...
... def go(self):
... print("Go")
...
... def before_cycle(self, event: str, source: State, target: State):
... print(f"Running {event} from {source.id} to {target.id}")
>>> sm = TrafficLightMachine()
>>> [sm.send("cycle") for _ in range(3)]
Slowdown
Running cycle from green to yellow
Stop
Running cycle from yellow to red
Go
Running cycle from red to green
[[None, None], [None, None], [None, None]]
While we've figured out a way to keep near complete backwards compatible changes to the new
{ref}Run to completion (RTC) by default feature (all built-in examples run without change),
if you encounter problems when upgrading to this version, you can still switch back to the old
{ref}Non-RTC model. Be aware that we may remove the {ref}Non-RTC model in the future.
StateMachine.run removed in favor of StateMachine.sendfrom tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
sm.run("cycle")
Should become:
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
>>> sm = TrafficLightMachine()
>>> sm.send("cycle")
Running cycle from green to yellow
StateMachine.allowed_transitions removed in favor of StateMachine.allowed_eventsfrom tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
assert [t.name for t in sm.allowed_transitions] == ["cycle"]
Should become:
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
>>> sm = TrafficLightMachine()
>>> assert [t.name for t in sm.allowed_events] == ["cycle"]
Statemachine.is_<state> removed in favor of StateMachine.<state>.is_activefrom tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
assert sm.is_green
Should become:
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
>>> sm = TrafficLightMachine()
>>> assert sm.green.is_active
State.identification removed in favor of State.idfrom tests.examples.traffic_light_machine import TrafficLightMachine
sm = TrafficLightMachine()
assert sm.current_state.identification == "green"
Should become:
>>> from tests.examples.traffic_light_machine import TrafficLightMachine
>>> sm = TrafficLightMachine()
>>> assert sm.current_state.id == "green"
StateMachine 1.0.3 fixes a bug between {ref}State and {ref}transition instances sharing references of callbacks when there were multiple concurrent in
January 27, 2023
StateMachine 1.0.3 fixes a bug between {ref}State and {ref}transition instances sharing
references of callbacks when there were multiple concurrent instances of the same StateMachine
class.
StateMachine class.January 27, 2023
StateMachine 1.0.3 fixes a bug between {ref}State and {ref}transition instances sharing
references of callbacks when there were multiple concurrent instances of the same StateMachine
class.
StateMachine class.StateMachine 1.0.2 fixes a regression bug blocking the library usage on Python 3.11.
January 12, 2023
StateMachine 1.0.2 fixes a regression bug blocking the library usage on Python 3.11.
These release notes cover the [](#whats-new-in-10), as well as some backwards incompatible changes you'll want to be aware of when upgrading from Stat…
January 11, 2023
Welcome to StateMachine 1.0!
This version is a huge refactoring adding a lot of new and exiting features. We hope that you enjoy.
These release notes cover the , as well as some backwards incompatible changes you'll want to be aware of when upgrading from StateMachine 0.9.0 or earlier. We've begun the deprecation process for some features.
StateMachine 1.0 supports Python 2.7, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10, and 3.11.
This is the last release to support Python 2.7, 3.5 and 3.6.
Transitions now support cond and unless parameters, to restrict
the execution.
class ApprovalMachine(StateMachine):
"A workflow machine"
requested = State("Requested", initial=True)
accepted = State("Accepted")
rejected = State("Rejected")
completed = State("Completed")
validate = requested.to(accepted, cond="is_ok") | requested.to(rejected)
You can generate diagrams from your statemachine.
Example:
Every single callback, being actions or guards, is now handled equally by the library.
Also, we've improved the internals in a way that you can implement your callbacks with any
number of arbritrary positional or keyword arguments (*args, **kwargs), and the dispatch will
match the available arguments with your method signature.
This means that if on your on_enter_<state>() or on_execute_<event>() method, you also
need to know the source (state), or the event (event), or access a keyword
argument passed with the trigger, you're covered. Just add this parameter to the method and It
will be passed by the dispatch mechanics.
Example of what's available:
def action_or_guard_method_name(self, *args, event_data, event, source, state, model, **kwargs):
pass
Observers are a way do generically add behaviour to a StateMachine without changing it's internal implementation.
The StateMachine itself is registered as an observer, so by using StateMachine.add_observer()
an external object can have the same level of functionalities provided to the built-in class.
StateMachine class.state is now entered when the machine starts. The actions, if defined,
on_enter_state and on_enter_<state> are now called.Prior to this release, as we didn't have validators-and-guards, there wasn't an elegant way
to declare multiples target states starting from the same pair (event, state). But the library
allowed a near-hackish way, by declaring a target state as the result of the on_<event> callback.
So, the previous code (not valid anymore):
class ApprovalMachine(StateMachine):
"A workflow machine"
requested = State('Requested', initial=True)
accepted = State('Accepted')
rejected = State('Rejected')
validate = requested.to(accepted, rejected)
def on_validate(self, current_time):
if self.model.is_ok():
self.model.accepted_at = current_time
return self.accepted
else:
return self.rejected
Should be rewriten to use guards, like this:
class ApprovalMachine(StateMachine):
"A workflow machine"
requested = State("Requested", initial=True)
accepted = State("Accepted")
rejected = State("Rejected")
validate = requested.to(accepted, conditions="is_ok") | requested.to(rejected)
def on_validate(self, current_time):
self.model.accepted_at = current_time
This issue was reported at #265.
Now StateMachine will execute the actions associated with the on_enter_state and
on_enter_<state> when initialized, if they exists.
Statemachine integrity checks are now performed at class declaration (import time) instead of on instance creation. This allows early feedback of invalid definitions.
This was the previous behaviour, you only got an error when trying to instantiate a StateMachine:
class CampaignMachine(StateMachine):
"A workflow machine"
draft = State('Draft', initial=True)
producing = State('Being produced')
closed = State('Closed', initial=True) # Should raise an Exception when instantiated
add_job = draft.to(draft) | producing.to(producing)
produce = draft.to(producing)
deliver = producing.to(closed)
with pytest.raises(exceptions.InvalidDefinition):
CampaignMachine()
Not this is performed as the class definition is performed:
with pytest.raises(exceptions.InvalidDefinition):
class CampaignMachine(StateMachine):
"A workflow machine"
draft = State("Draft", initial=True)
producing = State("Being produced")
closed = State(
"Closed", initial=True
) # Should raise an Exception right after the class is defined
add_job = draft.to(draft) | producing.to(producing)
produce = draft.to(producing)
deliver = producing.to(closed)
TransitionNotAllowed changed internal attr from transition to event.CombinedTransition does not exist anymore. State now holds a flat Transition list
called TransitionList that implements de OR operator. This turns a valid StateMachine
traversal much easier: [transition for state in machine.states for transition in state.transitions].StateMachine.get_transition is removed. See event.MultipleStatesFound and MultipleTransitionCallbacksFound are removed.
Since now you can have more than one callback defined to the same transition.on_enter_state and on_exit_state now accepts any combination of parameters following the
dynamic-dispatch rules. Previously it only accepted the state param.Transition.__init__ param on_execute renamed to simply on, and now follows the
dynamic-dispatch.Transition.destinations removed in favor of Transition.target (following SCXML convention).
Now each transition only points to a unique target. Each source->target pair is holded by a
single Transition.StateMachine.run is deprecated in favor of StateMachine.send.StateMachine.allowed_transitions is deprecated in favor of StateMachine.allowed_events.Statemachine.is_<state> is deprecated in favor of StateMachine.<state>.is_active.State.identification is deprecated in favor of State.id.January 11, 2023
Welcome to StateMachine 1.0.1!
This version is a huge refactoring adding a lot of new and exciting features. We hope that you enjoy it.
These release notes cover the new features in 1.0, as well as some backward incompatible changes you'll want to be aware of when upgrading from StateMachine 0.9.0 or earlier. We've begun the deprecation process for some features.
StateMachine 1.0 supports Python 2.7, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10, and 3.11.
This is the last release to support Python 2.7, 3.5, and 3.6.
Transitions now support cond and unless parameters, to restrict
the execution.
class ApprovalMachine(StateMachine):
"A workflow machine"
requested = State("Requested", initial=True)
accepted = State("Accepted")
rejected = State("Rejected")
completed = State("Completed")
validate = requested.to(accepted, cond="is_ok") | requested.to(rejected)
See {ref}`validators and guards` for more details.
You can generate diagrams from your state machine.
Example:
:caption: OrderControl
See {ref}`diagrams` for more details.
Every single callback, being {ref}actions or {ref}guards, is now handled equally by the library.
Also, we've improved the internals in a way that you can implement your callbacks with any
number of arbitrary positional or keyword arguments (*args, **kwargs), and the dispatch will
match the available arguments with your method signature.
This means that if on your on_enter_<state>() or on_execute_<event>() method, you also
need to know the source ({ref}state), or the event ({ref}event), or access a keyword
argument passed with the trigger, you're covered. Just add this parameter to the method and It
will be passed by the dispatch mechanics.
Example of what's available:
def action_or_guard_method_name(self, *args, event_data, event, source, state, model, **kwargs):
pass
See {ref}`dynamic-dispatch` for more details.
Observers are a way do generically add behavior to a StateMachine without changing it's internal implementation.
The StateMachine itself is registered as an observer, so by using StateMachine.add_observer()
an external object can have the same level of functionalities provided to the built-in class.
See {ref}`observers` for more details.
StateMachine class.state is now entered when the machine starts. The {ref}actions, if defined,
on_enter_state and on_enter_<state> are now called.Prior to this release, as we didn't have {ref}validators and guards, there wasn't an elegant way
to declare multiple target states starting from the same pair (event, state). But the library
allowed a near-hackish way, by declaring a target state as the result of the on_<event> callback.
So, the previous code (not valid anymore):
class ApprovalMachine(StateMachine):
"A workflow machine"
requested = State("Requested", initial=True)
accepted = State("Accepted")
rejected = State("Rejected")
validate = requested.to(accepted, rejected)
def on_validate(self, current_time):
if self.model.is_ok():
self.model.accepted_at = current_time
return self.accepted
else:
return self.rejected
Should be rewritten to use {ref}guards, like this:
class ApprovalMachine(StateMachine):
"A workflow machine"
requested = State("Requested", initial=True)
accepted = State("Accepted")
rejected = State("Rejected")
validate = requested.to(accepted, conditions="is_ok") | requested.to(rejected)
def on_validate(self, current_time):
self.model.accepted_at = current_time
See {ref}`validators and guards` of more details.
This issue was reported at #265.
Now StateMachine will execute the actions associated with the on_enter_state and
on_enter_<state>` when initialized if they exist.
See {ref}`State actions` for more details.
Statemachine integrity checks are now performed at class declaration (import time) instead of on instance creation. This allows early feedback on invalid definitions.
This was the previous behavior, you only got an error when trying to instantiate a StateMachine:
class CampaignMachine(StateMachine):
"A workflow machine"
draft = State("Draft", initial=True)
producing = State("Being produced")
closed = State("Closed", initial=True) # Should raise an Exception when instantiated
add_job = draft.to(draft) | producing.to(producing)
produce = draft.to(producing)
deliver = producing.to(closed)
with pytest.raises(exceptions.InvalidDefinition):
CampaignMachine()
Not this is performed as the class definition is performed:
with pytest.raises(exceptions.InvalidDefinition):
class CampaignMachine(StateMachine):
"A workflow machine"
draft = State("Draft", initial=True)
producing = State("Being produced")
closed = State(
"Closed", initial=True
) # Should raise an Exception right after the class is defined
add_job = draft.to(draft) | producing.to(producing)
produce = draft.to(producing)
deliver = producing.to(closed)
TransitionNotAllowed changed internal attr from transition to event.CombinedTransition does not exist anymore. {ref}State now holds a flat {ref}Transition list
called TransitionList that implements de OR operator. This turns a valid StateMachine
traversal much easier: [transition for state in machine.states for transition in state.transitions].StateMachine.get_transition is removed. See {ref}event.MultipleStatesFound and MultipleTransitionCallbacksFound are removed.
Since now you can have more than one callback defined to the same transition.on_enter_state and on_exit_state now accepts any combination of parameters following the
{ref}dynamic-dispatch rules. Previously it only accepted the state param.Transition.__init__ param on_execute renamed to simply on, and now follows the
{ref}dynamic-dispatch.Transition.destinations removed in favor of Transition.target (following SCXML convention).
Now each transition only points to a unique target. Each source->target pair is held by a
single Transition.StateMachine.run is deprecated in favor of StateMachine.send.StateMachine.allowed_transitions is deprecated in favor of StateMachine.allowed_events.Statemachine.is_<state> is deprecated in favor of StateMachine.<state>.is_active.State.identification is deprecated in favor of State.id.This release tag was replaced by 1.0.1 due to an error on the metadata when uploading to pypi.
January 11, 2023
This release tag was replaced by 1.0.1 due to an error on the metadata when uploading to pypi.
StateMachine 0.9 supports Python 2.7, 3.5, 3.6, 3.7, 3.8.
2022-12-21
StateMachine 0.9 supports Python 2.7, 3.5, 3.6, 3.7, 3.8.
Parameters sent with the event trigger will now be propagated to the transition handlers.
>>> from statemachine import StateMachine, State
>>> class CampaignMachine(StateMachine):
... draft = State("Draft", initial=True)
... producing = State("Being produced")
...
... produce = draft.to(producing) | producing.to(producing)
...
... def on_enter_producing(self, approver=None):
... print(f"Approved by: {approver}")
>>> sm = CampaignMachine()
>>> sm.produce(approver="Aragorn") # imperative
Approved by: Aragorn
Now you can declare final states and the machine will make sure they have no transitions.
>>> from statemachine import StateMachine, State
>>> class ApprovalMachine(StateMachine):
... """A workflow machine"""
... requested = State("Requested", initial=True)
... accepted = State("Accepted")
... rejected = State("Rejected")
... completed = State("Completed", final=True)
...
... validate = requested.to(accepted, cond="is_ok") | requested.to(rejected)
... release = accepted.to(completed)
... reopen = completed.to(requested)
Traceback (most recent call last):
...
InvalidDefinition: Cannot declare transitions from final state. Invalid state(s): ['completed']
See {ref}final-state for more details.
State machine names should now be fully qualified for mixins, simple names are deprecated and will no longer be supported on a future version.
2020-01-23
transition = state_a.from_(state_b).
Thanks @romulorosa.Fix Django integration for registry loading statemachine modules on Django1.7+.
2019-01-18
New event callbacks: on_enter_ and on_exit_ .
2018-04-01
on_enter_<state> and on_exit_<state>.# StateMachine 0.6.2 *2017-08-25* - Fix README.
2017-08-25
# StateMachine 0.6.1 *2017-08-25* - Fix deploy issues.
2017-08-25
Auto-discovering statemachine/statemachines under a Django project when they are requested using the mixin/registry feature.
2017-08-25
statemachine/statemachines under a Django project when
they are requested using the mixin/registry feature.Fix bug on CombinedTransition._can_run not allowing transitions to run if there are more than two transitions combined.
2017-07-24
CombinedTransition._can_run not allowing transitions to run if there are more than
two transitions combined.Duplicated definition of on_execute callback is not allowed.
2017-07-13
on_execute callback is not allowed.StateMachine.on_<transition.identifier> being called with extra self param.Nothing published for this version
Nothing published for this version
Drop official support for Python 3.3.
2017-07-10
Python 3.6 support.
Drop official support for Python 3.3.
Transition can be used as decorator for on_execute callback definition.
Transition can point to multiple destination states.
Nothing published for this version
README getting started section.
2017-03-22
State can hold a value that will be assigned to the model as the state value.
2017-03-22
State can hold a value that will be assigned to the model as the state value.# StateMachine 0.1.0 *2017-03-21* * First release on PyPI.
2017-03-21
Your coding agent can read these notes before it upgrades. Set up the MCP server →