Skip to content

StateMachine 2.6.0

Choose a tag to compare

@fgmacedo fgmacedo released this 13 Feb 23:45

StateMachine 2.6.0

February 2026

What's new in 2.6.0

This release adds the StateMachine.enabled_events method, Python 3.14 support,
a significant performance improvement for callback dispatch, and several bugfixes
for async condition expressions, type checker compatibility, and Django integration.

Python compatibility in 2.6.0

StateMachine 2.6.0 supports Python 3.7, 3.8, 3.9, 3.10, 3.11, 3.12, 3.13, and 3.14.

Checking enabled events

A new StateMachine.enabled_events method lets you query which events have their
cond/unless guards currently satisfied, going beyond StateMachine.allowed_events
which only checks reachability from the current state.

This is particularly useful for UI scenarios where you want to enable or disable buttons
based on whether an event's conditions are met at runtime.

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

Performance: cached signature binding

Callback dispatch is now significantly faster thanks to cached signature binding in
SignatureAdapter. The first call to a callback computes the argument binding and
caches a fast-path template; subsequent calls with the same argument shape skip the
full binding logic.

This results in approximately 60% faster bind_expected() calls and
around 30% end-to-end improvement on hot transition paths.

See #548 for benchmarks.

Bugfixes in 2.6.0

  • Fixes #531 domain model
    with falsy __bool__ was being replaced by the default Model().
  • Fixes #535 async predicates
    in condition expressions (not, and, or) were not being awaited, causing guards to
    silently return incorrect results.
  • Fixes #548
    VAR_POSITIONAL and kwargs precedence bugs in the signature binding cache introduced
    by the performance optimization.
  • Fixes #511 Pyright/Pylance
    false positive "Argument missing for parameter f" when calling events. Static analyzers
    could not follow the metaclass transformation from TransitionList to Event.
  • Fixes #551 MachineMixin
    now gracefully skips state machine initialization for Django historical models in data
    migrations, instead of raising ValueError.
  • Fixes #526 sanitize project
    path on Windows for documentation builds.

Misc in 2.6.0

  • Added Python 3.14 support #552.
  • Upgraded dev dependencies: ruff to 0.15.0, mypy to 1.14.1
    #552.
  • Clarified conditional transition evaluation order in documentation
    #546.
  • Added pydot DPI resolution settings to diagram documentation
    #514.
  • Fixed miscellaneous typos in documentation
    #522.
  • Removed Python 3.7 from CI build matrix
    ef351d5.