Python Target Documentation

Python Target Documentation

VSCXML generates Python 3.10+ code from W3C SCXML state machine specifications.

W3C Compliance Status

Datamodel Status Tests Passing
ECMAScript (dukpy) 100% 188/188
Null 100% 6/6
Native-Python 100% 23/23

The Python target achieves full W3C SCXML compliance across all datamodels, matching Java, JavaScript, and C# targets.

Requirements

  • Python 3.10 or later (uses match/case syntax)
  • dukpy >= 0.3.0 (for ECMAScript datamodel)

Installation

The generator produces a ready-to-run Python package with the runtime bundled locally:

bash
scxml-gen input.scxml -t python -o output/my_machine.py

cd output/
pip install -e .    # installs dukpy and the bundled scxmlgen package
python main.py      # runs the generated demo entry point

Generated project layout:

output/
├── my_machine.py         # Generated state machine
├── main.py               # Ready-to-run demo entry point
├── pyproject.toml        # pip project configuration
├── README.md
└── scxmlgen/             # Bundled runtime package
    ├── transpiled_state_machine.py
    ├── executor/
    ├── trace/
    └── ...

Code-only mode: Use --code-only to generate just the .py file when integrating into an existing project that already has scxmlgen installed.

Published Package

bash
pip install scxmlgen

Code Generation

Generate Python code using the CLI:

bash
# Full project (default) — generates pyproject.toml, main.py, scxmlgen/ runtime, README
scxml-gen input.scxml -t python -o output/my_machine.py

# Code only — single .py file, no project scaffold
scxml-gen input.scxml -t python -o my_machine.py --code-only

# Bundle invoke children
scxml-gen parent.scxml -t python -o output/parent.py --bundle-auto

Usage

Basic Example

python
from my_state_machine import MyStateMachine

# Create and start
machine = MyStateMachine()
machine.start()

# Send events
machine.send("go")
machine.send("complete", {"result": 42})

# Check current state
if machine.is_in_state("active"):
    print("Machine is active")

# Check completion
if machine.is_finished:
    print(f"Final states: {machine.final_states}")

Event Handling

python
from scxmlgen import Event
from my_state_machine import MyStateMachine

machine = MyStateMachine()
machine.start()

# String-based (convenient)
machine.send("start")

# Integer-based O(1) dispatch (use EVT_* constants for performance)
machine.send_by_id(MyStateMachine.EVT_START)

# Event with data (both methods)
machine.send("update", {"count": 5, "name": "test"})
machine.send_by_id(MyStateMachine.EVT_UPDATE, {"count": 5, "name": "test"})

# Full event construction
event = Event.builder() \
    .name("custom") \
    .type("external") \
    .data(key1="value1", key2=42) \
    .build()
machine.send(event)

Data Model Access

python
# Get variable
count = machine.get_variable("counter")

# Set variable (use with caution - prefer events)
machine.set_variable("counter", 10)

Logging

python
# Set a custom logger
machine.set_logger(lambda msg: print(f"[SM] {msg}"))

Data Models

ECMAScript (Default)

Uses dukpy for JavaScript expression evaluation. Supports basic state machines, transitions, conditions, scripts, and datamodel operations.

xml
<scxml datamodel="ecmascript">
    <datamodel>
        <data id="counter" expr="0"/>
        <data id="items" expr="[]"/>
    </datamodel>
    <state id="counting">
        <onentry>
            <script>counter = counter + 1; items.push(counter);</script>
        </onentry>
        <transition cond="counter >= 10" target="done"/>
    </state>
</scxml>

Python usage:

python
machine = MyMachine()
machine.start()
print(machine.get_variable("counter"))  # 10
print(machine.get_variable("items"))    # [1, 2, 3, ..., 10]

Null Datamodel

Minimal datamodel with only In() predicate support. No variables.

xml
<scxml datamodel="null">
    <state id="s0">
        <transition cond="In('s1')" target="fail"/>
        <transition target="pass"/>
    </state>
</scxml>

Native Python

Type-safe Python variables with compile-time code generation. Expressions are transformed to Python syntax at generation time.

xml
<scxml datamodel="native-python">
    <datamodel>
        <data id="counter" type="int" expr="0"/>
        <data id="name" type="str" expr="'default'"/>
        <data id="items" type="list[int]" expr="[]"/>
    </datamodel>
    <state id="s0">
        <transition cond="counter > 5" target="done"/>
        <onentry>
            <assign location="counter" expr="counter + 1"/>
        </onentry>
    </state>
</scxml>

Generated code:

python
class MyMachine(TranspiledStateMachine):
    def __init__(self):
        super().__init__("MyMachine", DataModelType.NATIVE_PYTHON, 2)
        self.counter: int = 0
        self.name: str = 'default'
        self.items: list[int] = []

    def _enter_S_S0(self) -> None:
        self._active_states.add(self.S_S0)
        self.counter = self.counter + 1  # Direct Python

    def _state_S_S0(self, event_id: int, event: Event) -> bool:
        if self.counter > 5:  # Direct Python condition
            self._exit_S_S0()
            self._enter_S_DONE()
            return True
        return False

Supported Types

  • int, float, str, bool
  • list[T], dict[K, V]
  • Any for untyped variables

Generated Code Structure

The generator produces a single Python file with:

  1. State Constants: Integer indices for O(1) lookups

    python
    S_IDLE = 0
    S_RUNNING = 1
  2. Event Constants: Integer IDs for fast dispatch

    python
    EVT_START = 1
    EVT_STOP = 2
  3. State Methods: Enter/exit/handler for each state

    python
    def _enter_S_IDLE(self) -> None: ...
    def _exit_S_IDLE(self) -> None: ...
    def _state_S_IDLE(self, event_id: int, event: Event) -> bool: ...
  4. Eventless Transitions: Three-phase execution for W3C compliance

    python
    def _do_eventless_step(self) -> bool: ...

Advanced Features

Delayed Events

Delayed sends are scheduled via the runtime context:

xml
<send event="timeout" delay="5s"/>

Process delayed events:

python
import time

machine.start()
while not machine.is_finished:
    machine.pump_events()
    time.sleep(0.1)

Invoke (Child State Machines)

Child state machines are bundled at generation time:

bash
scxml-gen parent.scxml -t python -o Parent.py --bundle-auto

The generated code includes an _INVOKE_REGISTRY for child lookups:

python
_INVOKE_REGISTRY: dict[str, type] = {
    "file:child.scxml": Parent_External_0,
    "s0.invoke.0": Parent_Child_0,
}

History States

Both shallow and deep history are supported:

python
# Shallow: remembers direct child
self._history_H0: int = -1

# Deep: remembers all active descendants
self._history_DEEP_H: set[int] | None = None

Parallel States

Parallel regions are entered/exited correctly:

python
def _enter_S_PARALLEL(self) -> None:
    self._active_states.add(self.S_PARALLEL)
    # Enter all parallel children
    self._enter_S_REGION1()
    self._enter_S_REGION2()

Tracing

Full tracing support for debugging and analysis:

python
from scxmlgen.trace import JsonlTraceWriter, ITraceListener

# JSONL trace writer (cross-platform compatible)
with JsonlTraceWriter("trace.jsonl") as writer:
    machine = MyStateMachine()
    machine.set_trace_listener(writer)
    machine.start()
    machine.send("go")

# Custom trace listener
class MyTraceListener(ITraceListener):
    def on_state_enter(self, session_id: str, state_id: str, timestamp_us: int) -> None:
        print(f"Entering state: {state_id}")

    def on_state_exit(self, session_id: str, state_id: str, timestamp_us: int) -> None:
        print(f"Exiting state: {state_id}")

    def on_transition(self, session_id: str, from_state: str, to_state: str,
                      event: str | None, timestamp_us: int) -> None:
        print(f"Transition: {from_state} -> {to_state} on {event}")

Executor (Thread-Safe, Autonomous Timers)

ContinuousExecutor runs the machine on a dedicated background thread and automatically processes delayed events (<send delay="..."/>) — no manual pump_events() calls required. The executor polls for due timers every 10 ms when idle.

python
from scxmlgen.executor import ContinuousExecutor, RunToCompletionExecutor
from my_state_machine import MyStateMachine

# ContinuousExecutor — background thread, autonomous timer handling
machine = MyStateMachine()

with ContinuousExecutor(machine) as executor:
    executor.start()

    # Send events (thread-safe, blocks until processed)
    executor.send("go")

    # Awaitable variant
    future = executor.send_async("complete")
    future.result()  # wait for processing

    while not machine.is_finished:
        import time; time.sleep(0.05)
# executor.shutdown() called automatically by the context manager

# RunToCompletionExecutor — synchronous, no background thread
machine2 = MyStateMachine()
executor2 = RunToCompletionExecutor(machine2)
executor2.start()
executor2.send("go")  # Processes on calling thread

Self-Contained Distribution

Bundle the runtime with generated code for pip-installable distribution:

bash
# Bundle runtime with generated code
scxml-gen input.scxml -t python -o output/ --bundle

# Creates:
# output/
#   my_state_machine.py    # Generated code
#   scxmlgen/              # Runtime library
#   pyproject.toml         # pip configuration
#   README.md              # Usage documentation

# Install bundled project
cd output && pip install -e .

Testing

Run W3C conformance tests:

bash
cd scxml-python/tests

# All tests
python run_tests.py --datamodel ecmascript

# Specific test
python run_tests.py --filter test144 --verbose

# Null datamodel
python run_tests.py --datamodel null

Performance Considerations

  • State tracking: Uses Python set[int] for O(1) state checks
  • Event dispatch: Dictionary lookup for event IDs
  • Memory: Generated code has minimal runtime overhead
  • dukpy: JavaScript evaluation adds ~2MB memory, ~1ms per expression

Troubleshooting

Import Error

If from scxmlgen import ... fails:

bash
pip install scxmlgen
# or
cd scxml-python && pip install -e .

dukpy Not Found

bash
pip install dukpy>=0.3.0

Python Version Error

The generated code requires Python 3.10+. Check your version:

bash
python --version

API Reference

TranspiledStateMachine

Method Description
start(init_data=None) Start the state machine
send(event_or_name, data=None) Send an event
pump_events() Process pending events
is_in_state(state_id) Check if state is active
is_finished Whether machine has terminated
active_states Set of active state IDs
final_states Final states (after finish)
get_variable(name) Get datamodel variable
set_variable(name, value) Set datamodel variable
set_logger(fn) Set logging callback

Event

Property Description
name Event name
type "platform", "internal", or "external"
sendid Send ID for delayed events
origin Origin URI
origintype Origin type
invokeid Invoke ID for child events
data Event data dictionary
raw_data Raw event data
Factory Method Description
Event.named(name) Simple event
Event.platform(name) Platform event
Event.internal(name) Internal event
Event.builder() Fluent builder

Event Introspection

python
all_events = machine.get_all_events()                    # frozenset
enabled = machine.get_enabled_events()                    # frozenset, guard-aware
for_state = machine.get_events_for_state("s1")           # frozenset
enabled_for = machine.get_enabled_events_for_state("s1") # frozenset