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 also

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.