Skip to content

The Event class is not fully supported in State.Compound #643

Description

@Dolecor

The documentation section Declaring events states that event can be declared with the Event class "for better IDE support (autocompletion, type checking) or to set a human-readable display name". It seems these advantages can not be used when the Event class is used inside the State.Compound class. When the event = Event(<transition>) form is used inside this class, the event is ignored, and <transition> becomes eventless.

I didn’t find any example in the documentation where events or transitions would be declared using the Event(<transition>) or event = Event(<transition>) forms inside the State.Compound class. However, I also didn’t find any restrictions regarding this.

Let's look at example from the Compound states section with and without Event(...) declaration.

  • The original example:

    from statemachine import State, StateChart
    
    class Journey(StateChart):
        class shire(State.Compound):
            bag_end = State(initial=True)
            green_dragon = State()
            visit_pub = bag_end.to(green_dragon)
        road = State(final=True)
        depart = shire.to(road)
    
  • And the same example, but with events visit_pub and depart declared using Event(...):

    class QuirkyJourney(StateChart):
        class shire(State.Compound):
            bag_end = State(initial=True)
            green_dragon = State()
            visit_pub = Event(bag_end.to(green_dragon))
        road = State(final=True)
        depart = Event(shire.to(road))
    

The following snippet shows that these machines are not identical:

j = Journey()
qj = QuirkyJourney()

print("Journey SM:")
print(f"  Configuration:  {j.configuration}")
print(f"  Enabled events: {j.enabled_events()}")
j._graph().write_png("j.png")

print("QuirkyJourney SM:")
print(f"  Configuration:  {qj.configuration}")
print(f"  Enabled events: {qj.enabled_events()}")
qj._graph().write_png("qj.png")

Output:

Journey SM:
  Configuration:  {Shire, Bag end}
  Enabled events: [BoundEvent('depart', delay=0, internal=False), BoundEvent('visit_pub', delay=0, internal=False)]
QuirkyJourney SM:
  Configuration:  {Shire, Green dragon}
  Enabled events: [BoundEvent('depart', delay=0, internal=False)]

Images:

  • Journey SM:
    Image

  • QuirkyJourney SM:
    Image

As you can see, the qj transitions to the green_dragon state without receiving the visit_pub event.

I checked the given example with python-statemachine 3.0.0, 3.1.0, 3.2.0 and 3.2.1. All have the same behavior.

Activity

  1. self-assigned this
    on Aug 16, 2026
  2. fgmacedo commented on Aug 16, 2026

    @fgmacedo
    Owner

    Thank you for the excellent report, @Dolecor. The side-by-side Journey / QuirkyJourney
    comparison, the diagrams and the version sweep made this one straightforward to pin down, and
    you were right that nothing in the docs restricts Event inside State.Compound.

    The bug is real and confirmed. A nested state class body is parsed by a different code path than
    the state machine class body, and that path only knew about the assignment form
    (visit_pub = bag_end.to(green_dragon)). It matched States, HistoryState, State and
    TransitionList, then fell through to a generic "is it callable?" branch. An Event is
    callable, so it landed there: the name got bound to a detached placeholder event and the
    transition it wrapped never received one, becoming eventless. That is exactly why QuirkyJourney
    starts already in green_dragon.

    Chasing it turned up a second casualty of the same gap: the @<source>.to(<target>) decorator
    also declares an event, and inside a compound body it registered no event and silently dropped
    its inline action. Both are fixed together in #645, which also adds regression tests for display
    names, explicit id=, combined transitions, the error_ prefix, and parallel regions.

    One divergence remains and is documented rather than silently left behind: an Event declared in
    a nested body with no transitions at all (knock = Event()) stays reachable as a class attribute
    but is not added to the machine's event list, unlike the top-level form.

    Thanks again for taking the time to write this up so carefully.

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

Metadata

Metadata

Assignees

Labels

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions