A robust Python library for parsing and running XState JSON state machines.
Project description
🚦 XState StateMachine for Python
A robust, asynchronous, and feature-complete Python library for parsing and executing state machines defined in XState-compatible JSON.
This library brings the power and clarity of formal state machines and statecharts, as popularized by XState, to the Python ecosystem. It allows you to define complex application logic as a clear, traversable graph and execute it in a fully asynchronous, predictable, and debuggable way.
Define your logic once in a simple JSON format, and use this library to bring it to life in your Python application.
🧭 Core Philosophy: Definition vs. Implementation
Definition (The "What"): You define your state machine's structure, states, and transitions in a JSON file. This is your blueprint. It describes what can happen.
Implementation (The "How"): You write the business logic—the actual code that runs—in Python. This describes how actions are performed or services are called.
This separation makes your application logic easier to understand, test, and maintain.
🎨 Design Your Logic Visually with the Stately Editor
One of the biggest advantages of using an XState-compatible format is the ability to visualize, design, and even simulate your logic using a graphical interface. The official Stately Editor allows you to drag-and-drop states, define transitions, and export the resulting JSON directly for use with this library.
Start designing at the Stately Editor →
✨ Key Features
- XState Compatible: Parses JSON configurations generated from the XState ecosystem.
- Fully Asynchronous: Built on
asynciofor modern, non-blocking applications. - Hierarchical & Parallel States: Model complex logic with nested and parallel states.
- Automatic Logic Discovery: Optionally, let the library find and bind your Python functions to your machine's logic automatically, reducing boilerplate.
- Timed Events: Use
afterfor declarative, time-based transitions. - Asynchronous Services: Use
invoketo call async functions and react to their success (onDone) or failure (onError). - Actor Model: Spawn child state machines from a parent machine for concurrent, isolated logic.
- Guards: Implement conditional transitions with simple guard functions.
- Developer Friendly: Full type hinting and a
LoggingInspectorplugin for easy debugging.
📦 Installation
Install the library directly from PyPI:
pip install xstate-statemachine
🚀 Getting Started: A Simple Example
Let's create a simple toggle switch.
1. Define the Machine (toggle.json)
{
"id": "toggle",
"initial": "inactive",
"states": {
"inactive": {
"on": {
"TOGGLE": "active"
}
},
"active": {
"on": {
"TOGGLE": "inactive"
}
}
}
}
2. Implement and Run (main.py)
import asyncio
import json
from xstate_statemachine import create_machine, Interpreter
async def main():
with open("toggle.json") as f:
toggle_config = json.load(f)
# Since there's no custom logic, we can create the machine directly.
toggle_machine = create_machine(toggle_config)
interpreter = await Interpreter(toggle_machine).start()
print(f"Initial state: {interpreter.current_state_ids}")
await interpreter.send("TOGGLE")
await asyncio.sleep(0.01)
print(f"New state: {interpreter.current_state_ids}")
await interpreter.stop()
if __name__ == "__main__":
asyncio.run(main())
🧠 Core Concepts
There are two primary ways to provide your Python implementations (actions, guards, services) to the state machine: Explicit Binding (the classic way) and Automatic Discovery (the new, convenient way).
🤖 Automatic Logic Discovery (Convention over Configuration)
This is the recommended approach for quickly wiring up your logic. Instead of manually creating a MachineLogic object, you can place your implementation functions in a Python module and tell the factory where to find them.
How it Works: The LogicLoader inspects your modules, finds your functions, and automatically binds them to the names in your JSON config.
Naming Convention: It's smart about names! A camelCase name in your JSON (like myAction) will automatically match a snake_case function in your Python code (def my_action(...):).
Example
-
Your Logic (
my_logic.py)# No MachineLogic object needed here! Just define your functions. def my_action(interpreter, context, event, action_def): print("Action executed!")
-
Your Runner (
main.py)from xstate_statemachine import create_machine # Tell the factory where to find your logic functions. machine = create_machine( config=my_config, logic_modules=["my_logic"] # Pass the module path as a string ) # ... or ... import my_logic machine = create_machine( config=my_config, logic_modules=[my_logic] # Pass the imported module object )
Actions & Context
Actions are "fire-and-forget" functions executed during transitions.
drone.json
{
"id": "drone",
"initial": "flying",
"context": { "battery": 100 },
"states": {
"flying": { "on": { "PHOTO_TAKEN": { "actions": ["decrementBattery"] } } }
}
}
drone_logic.py
# With Automatic Discovery, this is all you need.
def decrement_battery(interpreter, context, event, action_def):
context["battery"] -= 1
print(f"Battery at {context['battery']}%")
main.py
from xstate_statemachine import create_machine, MachineLogic
# 🤖 Option 1: Automatic Discovery (Recommended)
machine = create_machine(drone_config, logic_modules=["drone_logic"])
# 🧠 Option 2: Explicit Binding (Classic)
from drone_logic import decrement_battery
logic = MachineLogic(actions={"decrementBattery": decrement_battery})
machine = create_machine(drone_config, logic=logic)
Guards
Guards are conditions that must return True for a transition to be taken.
checkout.json
{
"id": "cart",
"context": { "items": [] },
"on": { "CHECKOUT": { "target": "paying", "guard": "cartIsNotEmpty" } }
}
checkout_logic.py
# With Automatic Discovery, this is all you need.
def cart_is_not_empty(context, event):
return len(context.get("items", [])) > 0
main.py
from xstate_statemachine import create_machine, MachineLogic
# 🤖 Option 1: Automatic Discovery (Recommended)
machine = create_machine(cart_config, logic_modules=["checkout_logic"])
# 🧠 Option 2: Explicit Binding (Classic)
from checkout_logic import cart_is_not_empty
logic = MachineLogic(guards={"cartIsNotEmpty": cart_is_not_empty})
machine = create_machine(cart_config, logic=logic)
Asynchronous Services (invoke)
Use invoke for long-running or async operations. The machine will transition based on the success (onDone) or failure (onError) of the invoked async function.
fetch.json
{
"id": "fetcher",
"initial": "loading",
"states": {
"loading": {
"invoke": {
"src": "fetchUserData",
"onDone": { "target": "success" },
"onError": { "target": "failure" }
}
},
"success": {}, "failure": {}
}
}
fetch_logic.py
import aiohttp
# With Automatic Discovery, this is all you need.
async def fetch_user_data(interpreter, context, event):
async with aiohttp.ClientSession() as session:
async with session.get("https://api.example.com/user") as resp:
resp.raise_for_status()
return await resp.json()
main.py
from xstate_statemachine import create_machine, MachineLogic
# 🤖 Option 1: Automatic Discovery (Recommended)
machine = create_machine(fetch_config, logic_modules=["fetch_logic"])
# 🧠 Option 2: Explicit Binding (Classic)
from fetch_logic import fetch_user_data
logic = MachineLogic(services={"fetchUserData": fetch_user_data})
machine = create_machine(fetch_config, logic=logic)
Timed Events (after)
Declaratively schedule transitions after a delay (in milliseconds).
traffic_light.json
{
"id": "light",
"initial": "green",
"states": {
"green": { "after": { "30000": "yellow" } },
"yellow": { "after": { "5000": "red" } }
}
}
Parallel States
Model independent, concurrent regions within a machine. The parent onDone fires when all child regions finish.
build.json
{
"id": "build",
"type": "parallel",
"onDone": "success",
"states": {
"backend": { /* ... */ },
"frontend": { /* ... */ }
}
}
Actors (Spawning Machines)
Spawn child machines from a parent for isolated, concurrent logic. Entry actions use spawn_<serviceName>.
main_machine.py
import asyncio
from xstate_statemachine import create_machine, MachineLogic
# 1. Child machine
child_config = { "id": "pinger", "on": { "PING": { "actions": ["pong"] } } }
child_logic = MachineLogic(
actions={"pong": lambda i,c,e,a: asyncio.create_task(i.parent.send("PONG"))}
)
child_node = create_machine(child_config, child_logic)
# 2. Parent machine
parent_config = {
"id": "parent", "initial": "running",
"states": { "running": { "entry": ["spawn_pingerService"] } },
"on": { "PONG": "finished" }
}
parent_logic = MachineLogic(services={"pingerService": child_node})
parent_machine = create_machine(parent_config, logic=parent_logic)
🐞 Debugging with Plugins
Use plugins to hook into the interpreter lifecycle. The built-in LoggingInspector is great for detailed logs.
import logging
from xstate_statemachine import Interpreter, LoggingInspector
logging.basicConfig(level=logging.INFO)
interpreter = Interpreter(my_machine)
interpreter.use(LoggingInspector())
await interpreter.start()
🤝 Contributing
Contributions are welcome! Open an issue on our GitHub Issue Tracker.
📄 License
This project is licensed under the MIT License. See the LICENSE file for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file xstate_statemachine-0.2.1.tar.gz.
File metadata
- Download URL: xstate_statemachine-0.2.1.tar.gz
- Upload date:
- Size: 41.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.1 CPython/3.13.2 Windows/10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b81404869a9b0849eb78a7ff64a6ee01876cd1970341ada72ade8ea0f3b2c104
|
|
| MD5 |
90e04109fb6256fba11f33f21376f8ed
|
|
| BLAKE2b-256 |
f0f8bf58b8d637fcf9bf6a65697fe4224a1fd52a3e95531d7ee8efc06b6a1b5f
|
File details
Details for the file xstate_statemachine-0.2.1-py3-none-any.whl.
File metadata
- Download URL: xstate_statemachine-0.2.1-py3-none-any.whl
- Upload date:
- Size: 44.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: poetry/2.1.1 CPython/3.13.2 Windows/10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a2197143097d0f1a4806a3f8e1b9a2e954019ebf580e32a1346292a9c83973d6
|
|
| MD5 |
338eb0dd9d1ac6c8d64e7103eb17eac9
|
|
| BLAKE2b-256 |
62562ea374e9d605e34d41178b2daa595af7393146883156812bffa6f5bc04d5
|