Skip to content

v3.1.0

Choose a tag to compare

@fgmacedo fgmacedo released this 15 May 15:18

StateChart 3.1.0

May 15, 2026

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

What's new in 3.1.0

Text representations with format()

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

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

Supported formats:

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

See Text representations for details.

Formatter facade

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

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.

Mermaid diagram support

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

Three ways to use it:

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

See Mermaid format for details.

Auto-expanding docstrings

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

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.

Sphinx directive for inline diagrams

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

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

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

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

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

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

See Sphinx directive for full documentation.
#589.

Diagram CLI --events and --format options

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

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

See Command line for details.
#593.

Performance: 5x–7x faster event processing

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

Thread safety documentation

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

Coroutine functions as invoke targets

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

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.

Bugfixes in 3.1.0

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

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

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

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

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

Misc in 3.1.0

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

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

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

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