Python StateMachine v3.1.2 Release Notes

Release Date: 2026-05-19 // 24 days ago
  • 🐛 Bug fixes in 3.1.2

    pydot is optional again

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

    ⏪ Two changes restore the original behavior:

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

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

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

    #622.

    Other changes in 3.1.2

    🔒 Security and dependency bumps

    Picked up from the dependency refresh applied to main:

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

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


Previous changes from v3.1.1

  • StateChart 3.1.1

    May 15, 2026

    🐛 Bug fixes in 3.1.1

    🔧 Thread-safety hardening of the configuration cache

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

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

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

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

    🐎 Performance impact

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

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

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

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