Skip to content

Support structural refinement of inherited states while preserving transitions #660

Description

@BorjaEst

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:

  1. automatic refinement based on declaration identity;
  2. an explicit refinement API;
  3. 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:

  1. A subclass can refine an inherited atomic state into a compound state.

  2. Inherited transitions targeting that state enter the refined compound state.

  3. Entering the refined compound state activates its initial child.

  4. Inherited transitions sourced from the refined state remain applicable while one of its descendants is active.

  5. The subclass contains one logical state for the refined declaration, not separate inherited and replacement nodes.

  6. Constructing the subclass does not mutate the base statechart.

  7. Two subclasses can refine the same inherited state independently.

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions