Summary
python-statemachine supports both class inheritance and hierarchical statecharts, but these capabilities currently cannot be combined to structurally refine an inherited state.
A base StateChart can define a reusable topology:
from statemachine import State, StateChart
class ProgramStateChart(StateChart):
halted = State(initial=True)
ready = State()
running = State()
suspended = State()
reset = halted.to(ready)
start = ready.to(running)
suspend = running.to(suspended)
resume = suspended.to(running)
complete = running.to(ready)
A subclass may need to preserve that topology while refining running into a compound state:
class ExampleProgramStateChart(ProgramStateChart):
class running(State.Compound):
loading = State(initial=True)
processing = State()
unloading = State()
loaded = loading.to(processing)
processed = processing.to(unloading)
This currently produces an invalid statechart. The inherited transitions continue to refer to the inherited running state, while the subclass declares a different compound state under the same name.
The nested states are consequently treated as disconnected or unreachable.
Environment
Please replace these values as appropriate:
python-statemachine version:
Python version:
Operating system:
Reproduced on current develop: yes/no
Minimal reproduction
from statemachine import State, StateChart
class BaseMachine(StateChart):
idle = State(initial=True)
running = State()
start = idle.to(running)
class DerivedMachine(BaseMachine):
class running(State.Compound):
loading = State(initial=True)
processing = State()
loaded = loading.to(processing)
Observed behavior
The subclass definition fails validation with an error similar to:
statemachine.exceptions.InvalidDefinition:
There are unreachable states.
The statemachine graph should have a single component.
Disconnected states: ['loading', 'processing']
The resulting construction appears to contain two distinct concepts of running:
BaseMachine.running
referenced by the inherited start transition
DerivedMachine.running
compound state containing loading and processing
The subclass attribute shadows or replaces the inherited declaration, but the inherited transition endpoint is not resolved against the final subclass topology.
Expected behavior
It should be possible to explicitly or implicitly refine an inherited state while preserving inherited transitions that reference that state.
Conceptually, this:
BaseMachine:
Idle ──start──> Running
combined with:
DerivedMachine:
Running
└── Loading ──loaded──> Processing
should produce:
Idle
│
│ start
▼
Running
└── Loading
After entering running, its initial child should also become active:
machine = DerivedMachine()
machine.send("start")
assert "running" in machine.configuration_values
assert "loading" in machine.configuration_values
The inherited start transition should target the refined DerivedMachine.running, not a separate inherited state object.
The same should apply to inherited transitions whose source is the refined state. For example:
class BaseMachine(StateChart):
ready = State(initial=True)
running = State()
start = ready.to(running)
complete = running.to(ready)
If running is refined into a compound state, complete should remain applicable while one of its children is active and should exit the compound state normally.
Requested capability
The required capability is structural topology refinement:
inherited base topology
+
subclass-specific hierarchical refinement
=
one valid subclass topology
The request does not depend on a particular implementation mechanism. The library could provide either:
- automatic refinement based on declaration identity;
- an explicit refinement API;
- another supported construction mechanism that retains ordinary class inheritance.
The important semantic requirement is that the inherited state and its refined representation are treated as the same logical state within the subclass topology.
Possible API directions
1. Rebind inherited transition endpoints
When a subclass replaces an inherited state declaration with the same attribute name, inherited transitions referencing the original state could be copied or rebound to the subclass state before graph validation.
Conceptually:
inherited transition endpoint:
BaseMachine.running
subclass declaration:
DerivedMachine.running
subclass transition endpoint:
DerivedMachine.running
This rebinding would need to apply consistently to both transition sources and targets.
2. Resolve transition endpoints symbolically
Transitions could retain a symbolic declaration reference, such as "running", until construction of the final subclass topology is complete.
The metaclass could then resolve the endpoint against the effective subclass namespace.
This could avoid binding inherited transitions permanently to the concrete State object created for the base class.
3. Provide an explicit refinement API
If automatic rebinding by attribute name would be ambiguous or incompatible with the existing inheritance model, refinement could be explicit.
For example:
class DerivedMachine(BaseMachine):
class running(
State.Compound,
refines=BaseMachine.running,
):
loading = State(initial=True)
processing = State()
loaded = loading.to(processing)
Alternatively, the existing state could expose a refinement operation:
class DerivedMachine(BaseMachine):
running = BaseMachine.running.refine(
State.Compound,
states=...,
)
These are only API sketches. The essential behavior is that transitions inherited from the base class are updated or resolved against the refined state.
Important semantic questions
Some cases may require explicit rules:
- Is declaration identity determined by the Python attribute name, the state
id, or the inherited State object?
- What happens when a subclass uses the same attribute name but supplies a different explicit state
id?
- Should refinement be permitted only from an atomic state to a compound state, or also between other state kinds?
- How should multiple inheritance handle two inherited states with the same declaration name?
- Should replacing a state without an explicit refinement marker produce an error rather than silently rebinding transitions?
- Must transitions, history targets, callbacks, guards, eventless transitions, and other references all be rebound consistently?
- How are sibling subclasses isolated so that refining a state in one subclass cannot mutate the base class or another subclass?
An explicit API may be preferable if automatic name-based refinement cannot answer these questions safely.
Motivation
This capability is useful when a framework defines a standardized top-level state-machine contract while concrete implementations provide more detailed behavior inside particular states.
One example is OPC UA ProgramStateMachineType.
The standardized top-level states include:
Halted
Ready
Running
Suspended
with a standardized transition topology.
A concrete program may need additional behavior beneath one of those states:
Running
├── Loading
├── Processing
└── Unloading
OPC UA Part 10 provides an analogous modeling pattern in DomainDownloadType. It derives from ProgramStateMachineType, retains transitions from the base program state machine, and defines subordinate state machines associated with the standardized Running and Halted states.
A reusable Python representation therefore needs a way to preserve the standardized top-level topology while supplying implementation-specific subordinate behavior.
Without structural refinement, every concrete implementation must reconstruct the standardized topology after all concrete states are known. That duplicates the base contract and makes it easier for implementations to diverge from it.
Current workarounds
Rebuild the complete statechart
A factory or builder can create all states first and construct the transitions against the final objects:
ExampleProgramStateChart = build_program_state_chart(
running=ExampleRunning,
)
This can produce the correct graph, but it loses much of the clarity, type identity, callback organization, and ordinary extensibility provided by class inheritance.
Redeclare every affected transition
The subclass can redefine all inherited transitions whose source or target is the replaced state.
For a standardized topology, this is error-prone because the subclass must know and duplicate every relationship involving that state.
It also becomes difficult to distinguish intentional changes from transitions that were copied only to repair inherited object references.
Use composition instead of inheritance
The base topology can be wrapped by a second statechart or controller.
Composition may be appropriate for independent state machines, but it does not represent a subtype that preserves and structurally refines the original topology. It also requires additional synchronization between the outer and inner state configurations.
Suggested acceptance criteria
A solution should demonstrate at least the following:
-
A subclass can refine an inherited atomic state into a compound state.
-
Inherited transitions targeting that state enter the refined compound state.
-
Entering the refined compound state activates its initial child.
-
Inherited transitions sourced from the refined state remain applicable while one of its descendants is active.
-
The subclass contains one logical state for the refined declaration, not separate inherited and replacement nodes.
-
Constructing the subclass does not mutate the base statechart.
-
Two subclasses can refine the same inherited state independently.
-
Existing statechart inheritance behavior remains compatible, or incompatible cases fail with a clear definition-time error.
An illustrative behavioral test could have this shape:
from statemachine import State, StateChart
class BaseMachine(StateChart):
ready = State(initial=True)
running = State()
start = ready.to(running)
complete = running.to(ready)
class DerivedMachine(BaseMachine):
class running(State.Compound):
loading = State(initial=True)
processing = State()
loaded = loading.to(processing)
machine = DerivedMachine()
machine.send("start")
assert set(machine.configuration_values) == {"running", "loading"}
machine.send("loaded")
assert set(machine.configuration_values) == {"running", "processing"}
machine.send("complete")
assert set(machine.configuration_values) == {"ready"}
Related issues
This issue is therefore related to the existing inheritance and metaclass work but has a separate behavioral requirement and acceptance criteria.
Questions
Would structural refinement of an inherited state be compatible with the intended inheritance model of python-statemachine?
If automatic refinement is considered too ambiguous, would an explicit refinement API be acceptable?
If neither form is intended to be supported, what is the recommended pattern for defining reusable base topologies where subclasses must attach subordinate state machines to inherited states without duplicating the base transition graph?
Summary
python-statemachinesupports both class inheritance and hierarchical statecharts, but these capabilities currently cannot be combined to structurally refine an inherited state.A base
StateChartcan define a reusable topology:A subclass may need to preserve that topology while refining
runninginto a compound state:This currently produces an invalid statechart. The inherited transitions continue to refer to the inherited
runningstate, while the subclass declares a different compound state under the same name.The nested states are consequently treated as disconnected or unreachable.
Environment
Please replace these values as appropriate:
Minimal reproduction
Observed behavior
The subclass definition fails validation with an error similar to:
The resulting construction appears to contain two distinct concepts of
running:The subclass attribute shadows or replaces the inherited declaration, but the inherited transition endpoint is not resolved against the final subclass topology.
Expected behavior
It should be possible to explicitly or implicitly refine an inherited state while preserving inherited transitions that reference that state.
Conceptually, this:
combined with:
should produce:
After entering
running, its initial child should also become active:The inherited
starttransition should target the refinedDerivedMachine.running, not a separate inherited state object.The same should apply to inherited transitions whose source is the refined state. For example:
If
runningis refined into a compound state,completeshould remain applicable while one of its children is active and should exit the compound state normally.Requested capability
The required capability is structural topology refinement:
The request does not depend on a particular implementation mechanism. The library could provide either:
The important semantic requirement is that the inherited state and its refined representation are treated as the same logical state within the subclass topology.
Possible API directions
1. Rebind inherited transition endpoints
When a subclass replaces an inherited state declaration with the same attribute name, inherited transitions referencing the original state could be copied or rebound to the subclass state before graph validation.
Conceptually:
This rebinding would need to apply consistently to both transition sources and targets.
2. Resolve transition endpoints symbolically
Transitions could retain a symbolic declaration reference, such as
"running", until construction of the final subclass topology is complete.The metaclass could then resolve the endpoint against the effective subclass namespace.
This could avoid binding inherited transitions permanently to the concrete
Stateobject created for the base class.3. Provide an explicit refinement API
If automatic rebinding by attribute name would be ambiguous or incompatible with the existing inheritance model, refinement could be explicit.
For example:
Alternatively, the existing state could expose a refinement operation:
These are only API sketches. The essential behavior is that transitions inherited from the base class are updated or resolved against the refined state.
Important semantic questions
Some cases may require explicit rules:
id, or the inheritedStateobject?id?An explicit API may be preferable if automatic name-based refinement cannot answer these questions safely.
Motivation
This capability is useful when a framework defines a standardized top-level state-machine contract while concrete implementations provide more detailed behavior inside particular states.
One example is OPC UA
ProgramStateMachineType.The standardized top-level states include:
with a standardized transition topology.
A concrete program may need additional behavior beneath one of those states:
OPC UA Part 10 provides an analogous modeling pattern in
DomainDownloadType. It derives fromProgramStateMachineType, retains transitions from the base program state machine, and defines subordinate state machines associated with the standardizedRunningandHaltedstates.A reusable Python representation therefore needs a way to preserve the standardized top-level topology while supplying implementation-specific subordinate behavior.
Without structural refinement, every concrete implementation must reconstruct the standardized topology after all concrete states are known. That duplicates the base contract and makes it easier for implementations to diverge from it.
Current workarounds
Rebuild the complete statechart
A factory or builder can create all states first and construct the transitions against the final objects:
This can produce the correct graph, but it loses much of the clarity, type identity, callback organization, and ordinary extensibility provided by class inheritance.
Redeclare every affected transition
The subclass can redefine all inherited transitions whose source or target is the replaced state.
For a standardized topology, this is error-prone because the subclass must know and duplicate every relationship involving that state.
It also becomes difficult to distinguish intentional changes from transitions that were copied only to repair inherited object references.
Use composition instead of inheritance
The base topology can be wrapped by a second statechart or controller.
Composition may be appropriate for independent state machines, but it does not represent a subtype that preserves and structurally refines the original topology. It also requires additional synchronization between the outer and inner state configurations.
Suggested acceptance criteria
A solution should demonstrate at least the following:
A subclass can refine an inherited atomic state into a compound state.
Inherited transitions targeting that state enter the refined compound state.
Entering the refined compound state activates its initial child.
Inherited transitions sourced from the refined state remain applicable while one of its descendants is active.
The subclass contains one logical state for the refined declaration, not separate inherited and replacement nodes.
Constructing the subclass does not mutate the base statechart.
Two subclasses can refine the same inherited state independently.
Existing statechart inheritance behavior remains compatible, or incompatible cases fail with a clear definition-time error.
An illustrative behavioral test could have this shape:
Related issues
Read every statechart class body through one reader with an owning state #657 concerns unifying how top-level and nested statechart class bodies are processed. That construction work may provide a useful place to implement refinement, but it does not currently specify replacement of inherited states or rebinding of inherited transitions.
Subclassing a StateChart with callbacks in a State.Compound body raises AttributeError #646 concerns subclassing a
StateChartwhose compound-state body contains callbacks. It demonstrates that inherited statecharts currently reuse state objects, but its failure mode is destructive callback processing rather than structural state refinement.Subclassing a StateChart whose nested body has callbacks raises AttributeError on _callbacks #658 describes the same callback-inheritance failure in more detail but does not address replacement of an inherited state.
States lost when Subclassing a statemachine class with states #211 was an earlier issue concerning ordinary inheritance of states. Structural refinement of a state into a hierarchical state was not its subject.
This issue is therefore related to the existing inheritance and metaclass work but has a separate behavioral requirement and acceptance criteria.
Questions
Would structural refinement of an inherited state be compatible with the intended inheritance model of
python-statemachine?If automatic refinement is considered too ambiguous, would an explicit refinement API be acceptable?
If neither form is intended to be supported, what is the recommended pattern for defining reusable base topologies where subclasses must attach subordinate state machines to inherited states without duplicating the base transition graph?