This guide covers all backward-incompatible changes in python-statemachine 3.0 and provides step-by-step instructions for a smooth migration from the 2.x series.
Most 2.x code continues to work unchanged — the `StateMachine` class preserves backward-compatible
defaults. Review this guide to understand what changed and adopt the new APIs at your own pace.
- Upgrade Python to 3.9+ (3.7 and 3.8 are no longer supported).
- Replace
rtc=True/Falsein constructors — the non-RTC model has been removed. - Replace
allow_event_without_transitioninit parameter with a class-level attribute. - Replace
sm.current_statewithsm.configuration. - Replace
sm.add_observer(...)withsm.add_listener(...). - Update code that catches
TransitionNotAllowedand accesses.state→ use.configuration. - Review
oncallbacks that queryis_activeorcurrent_stateduring transitions. - If using
States.from_enum, note thatuse_enum_instancenow defaults toTrue. - If using
get_machine_cls()with short names, switch to fully-qualified names.
Support for Python 3.7 and 3.8 has been dropped. If you need these versions, stay on the 2.x series.
StateMachine 3.0 supports Python 3.9, 3.10, 3.11, 3.12, 3.13, and 3.14.
Version 3.0 introduces StateChart as the new base class. The existing StateMachine class is
now a subclass of StateChart with defaults that preserve 2.x behavior:
| Attribute | StateChart |
StateMachine |
|---|---|---|
allow_event_without_transition |
True |
False |
enable_self_transition_entries |
True |
False |
atomic_configuration_update |
False |
True |
error_on_execution |
True |
False |
Recommendation: We recommend migrating to StateChart for new code. It follows the
SCXML specification and enables powerful features like compound
states, parallel states, and structured error handling.
For existing code, you can continue using StateMachine — it works as before. You can also adopt
individual StateChart behaviors granularly by overriding class-level attributes:
# Adopt SCXML error handling without switching to StateChart
class MyMachine(StateMachine):
error_on_execution = True
# ... rest of your definition unchangedSee {ref}statecharts for full details on each attribute.
The rtc parameter was deprecated in 2.3.2 and has been removed. All events are now queued before
processing (Run-to-Completion semantics).
Before (2.x):
sm = MyMachine(rtc=False) # synchronous, non-queued processingAfter (3.0):
sm = MyMachine() # RTC is always enabled, remove the parameterIf you were passing rtc=True (the default), simply remove the parameter.
This was previously an __init__ parameter and is now a class-level attribute.
Before (2.x):
sm = MyMachine(allow_event_without_transition=True)After (3.0):
class MyMachine(StateMachine):
allow_event_without_transition = True
# ... states and transitions`StateMachine` defaults to `False` (same as 2.x). `StateChart` defaults to `True`.
Due to compound and parallel states, the state machine can now have multiple active states. The
current_state property is deprecated in favor of configuration, which always returns an
OrderedSet[State].
Before (2.x):
state = sm.current_state # returns a single State
value = sm.current_state.value # get the valueAfter (3.0):
states = sm.configuration # returns OrderedSet[State]
values = sm.configuration_values # returns OrderedSet of values
# If you know you have a single active state (flat machine):
state = next(iter(sm.configuration)) # get the single StateFor flat state machines (no compound/parallel states), `current_state_value` still returns a
single value and works as before. But we strongly recommend using `configuration` /
`configuration_values` for forward compatibility.
If you checked whether the machine had reached a final state via current_state.final, use the
new is_terminated property instead. It works correctly for all topologies (flat, compound, and
parallel).
Before (2.x):
if sm.current_state.final:
print("done")
while not sm.current_state.final:
sm.send("next")After (3.0):
if sm.is_terminated:
print("done")
while not sm.is_terminated:
sm.send("next")The method add_observer has been removed in v3.0. Use add_listener instead.
Before (2.x):
sm.add_observer(my_listener)After (3.0):
sm.add_listener(my_listener)In 2.x, States.from_enum defaulted to use_enum_instance=False, meaning state values were the
raw enum values (e.g., integers). In 3.0, the default is True, so state values are the enum
instances themselves.
Before (2.x):
states = States.from_enum(MyEnum, initial=MyEnum.start)
# states.start.value == 1 (raw value)After (3.0):
states = States.from_enum(MyEnum, initial=MyEnum.start)
# states.start.value == MyEnum.start (enum instance)If your code relies on raw enum values, pass use_enum_instance=False explicitly.
In 2.x, state machine classes were registered both by their fully-qualified name and their short class name. The short-name lookup was deprecated since v0.8 and has been removed in 3.0.
Before (2.x):
from statemachine.registry import get_machine_cls
cls = get_machine_cls("MyMachine") # short name — worked with warningAfter (3.0):
from statemachine.registry import get_machine_cls
cls = get_machine_cls("myapp.machines.MyMachine") # fully-qualified nameThe TransitionNotAllowed exception now stores a configuration attribute (a set of states)
instead of a single state attribute, and the event attribute can be None.
Before (2.x):
try:
sm.send("go")
except TransitionNotAllowed as e:
print(e.event) # Event instance
print(e.state) # single StateAfter (3.0):
try:
sm.send("go")
except TransitionNotAllowed as e:
print(e.event) # Event instance or None
print(e.configuration) # MutableSet[State]This is the most impactful behavioral change for existing code.
In 2.x, the active state was updated atomically after the transition on callbacks,
meaning sm.current_state and state.is_active reflected the source state during on
callbacks.
In 3.0 (SCXML-compliant behavior in StateChart), states are exited before on callbacks
and entered after, so during on callbacks the configuration may be empty.
If you use `StateMachine` (not `StateChart`), the default `atomic_configuration_update=True`
**preserves the 2.x behavior**. This section only affects code using `StateChart` or
`StateMachine` with `atomic_configuration_update=False`.
Before (2.x):
def on_validate(self):
if self.accepted.is_active: # True during on callback in 2.x
return "congrats!"After (3.0):
Two new keyword arguments are available in on callbacks to inspect the transition context:
def on_validate(self, previous_configuration, new_configuration):
if self.accepted in previous_configuration:
return "congrats!"previous_configuration: the set of states that were active before the microstep.new_configuration: the set of states that will be active after the microstep.
To restore the old behavior globally, set the class attribute:
class MyChart(StateChart):
atomic_configuration_update = True # restore 2.x behaviorOr simply use StateMachine, which has atomic_configuration_update=True by default.
In StateChart, self-transitions (a state transitioning to itself) now execute entry and exit
actions, following the SCXML spec. In StateMachine, the 2.x behavior is preserved (no
entry/exit on self-transitions).
Before (2.x):
# Self-transitions did NOT trigger on_enter_*/on_exit_* callbacks
loop = s1.to.itself()After (3.0 with StateChart):
# Self-transitions DO trigger on_enter_*/on_exit_* callbacks
loop = s1.to.itself()
# To disable (preserve 2.x behavior):
class MyChart(StateChart):
enable_self_transition_entries = FalseThe send() method has new optional parameters for delayed events and internal events:
# 2.x signature
sm.send("event_name", *args, **kwargs)
# 3.0 signature (fully backward compatible)
sm.send("event_name", *args, delay=0, send_id=None, internal=False, **kwargs)delay: Time in milliseconds before the event is processed.send_id: Identifier for the event, used to cancel delayed events withsm.cancel_event(send_id).internal: IfTrue, the event is placed in the internal queue (processed in the current macrostep).
Existing code calling sm.send("event") works unchanged.
The string representation now shows configuration=[...] instead of current_state=...:
Before (2.x):
MyMachine(model=Model(), state_field='state', current_state='initial')
After (3.0):
MyMachine(model=Model(), state_field='state', configuration=['initial'])
The package now exports two additional symbols:
from statemachine import StateChart # new base class
from statemachine import HistoryState # history pseudo-state for compound states
from statemachine import StateMachine # unchanged
from statemachine import State # unchanged
from statemachine import Event # unchangedFor full details on all new features, see the {ref}3.0.0 release notes <StateMachine 3.0.0>.
Here's a summary of what's new:
- Compound states — hierarchical state nesting with
State.Compound - Parallel states — concurrent regions with
State.Parallel - History pseudo-states — shallow and deep history with
HistoryState() - Eventless (automatic) transitions — transitions that fire when guard conditions are met
- DoneData on final states — final states can provide data to
done.statehandlers - Dynamic state machine creation —
create_machine_class_from_definition()from dicts In()conditions — check if a state is active in guard expressionsprepare_event()callback — inject custom kwargs into all other callbacks- SCXML-compliant event matching — wildcard events, dot notation
- Error handling —
error.executionevent for runtime exceptions - Delayed events —
send(..., delay=500)with cancellation support validate_disconnected_statesflag — disable single-component graph validationis_terminatedproperty — check if the state machine has reached a final stateraise_()method — send events to the internal queuecancel_event()method — cancel delayed events by ID