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/casesyntax) dukpy>= 0.3.0 (for ECMAScript datamodel)
Installation
Bundled Project (Default — Recommended)
The generator produces a ready-to-run Python package with the runtime bundled locally:
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-onlyto generate just the.pyfile when integrating into an existing project that already hasscxmlgeninstalled.
Published Package
pip install scxmlgen
Code Generation
Generate Python code using the CLI:
# 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
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
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
# Get variable
count = machine.get_variable("counter")
# Set variable (use with caution - prefer events)
machine.set_variable("counter", 10)
Logging
# 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.
<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:
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.
<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.
<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:
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,boollist[T],dict[K, V]Anyfor untyped variables
Generated Code Structure
The generator produces a single Python file with:
State Constants: Integer indices for O(1) lookups
pythonS_IDLE = 0 S_RUNNING = 1Event Constants: Integer IDs for fast dispatch
pythonEVT_START = 1 EVT_STOP = 2State Methods: Enter/exit/handler for each state
pythondef _enter_S_IDLE(self) -> None: ... def _exit_S_IDLE(self) -> None: ... def _state_S_IDLE(self, event_id: int, event: Event) -> bool: ...Eventless Transitions: Three-phase execution for W3C compliance
pythondef _do_eventless_step(self) -> bool: ...
Advanced Features
Delayed Events
Delayed sends are scheduled via the runtime context:
<send event="timeout" delay="5s"/>
Process delayed events:
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:
scxml-gen parent.scxml -t python -o Parent.py --bundle-auto
The generated code includes an _INVOKE_REGISTRY for child lookups:
_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:
# 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:
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:
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.
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:
# 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:
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:
pip install scxmlgen
# or
cd scxml-python && pip install -e .
dukpy Not Found
pip install dukpy>=0.3.0
Python Version Error
The generated code requires Python 3.10+. Check your version:
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
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