statem
A minimal async state machine engine for Python, built on Pydantic. The state graph is a validated dict; guards and actions are plain Python (sync or async) functions, registered by name.
Full documentation: https://rahuldas-dev.github.io/statem/
Install · Quickstart · Hooks · Visualizing · Why it's built this way · Development · License
Install
pip install statem
The importable package is statem:
from statem import StateMachine, Signal
Quickstart
import asyncio
from statem import Signal, StateMachine
config = {
"idle": {"on": {"START": {"target": "running", "guard": "can_start"}}},
"running": {"on": {"STOP": "idle"}},
}
def can_start(ctx, signal) -> bool:
return True
async def main() -> None:
machine = StateMachine.from_dict(config, guard_dict={"can_start": can_start})
state = await machine.run(state_name="idle", events=Signal(event="START"), session={})
print(state) # "running"
asyncio.run(main())
See examples/bread.py for a fuller example (guards,
actions, error_state, and an always-transition), or
examples/bank.py for a richer one that exercises
every hook in one conversation, including error_state recovering from a real exception. The
guide covers the full config shape.
Hooks
Four lifecycle hooks can carry guards and/or actions on any state:
| Hook | Fires when | Carries |
|---|---|---|
on |
An external Signal (event) matches this state's on map (or its "*" wildcard) |
Candidates tried in order: guard (optional, must return bool) gates the candidate; its actions run if it fires |
always |
Right after any state entry, including the initial one — no external signal needed | Same shape as on, minus the event name; used to auto-advance once a condition becomes true |
entry |
A state is entered (after the firing transition's own actions) |
Action names only |
exit |
A state is left, before entering the next one | Action names only |
A fifth field, error_state, isn't a hook itself but a per-state escape hatch: if an
on-transition's action raises, the engine catches it and — if error_state is set — transitions
there instead of propagating.
Visualizing
to_mermaid(machine, initial=None) renders machine.config as a Mermaid stateDiagram-v2
string — no extra dependency, just text. GitHub, MkDocs, VS Code, and Jupyter all render it
natively:
from statem import to_mermaid
print(to_mermaid(machine, initial="idle"))
stateDiagram-v2
[*] --> idle
idle --> running: START [can_start]
running --> idle: STOP
See the guide for guard
chains, always, and error_state rendering.
Why it's built this way
- Data and behavior stay apart.
configis a plain, JSON/YAML-friendly dict — author it by hand, generate it, or load it from a file/database/LLM. Guards and actions are ordinary functions registered by name, independent of the graph. - Fails fast. Bad transition targets and unregistered guard/action names raise at construction time, not three hops into a live run.
- Sync or async, your choice. The engine detects and awaits either correctly — no need to
wrap trivial sync logic in
async def. sessionis yours. Any shape you want (dict, dataclass, ORM row) — the engine never inspects it, just threads it through to your guards/actions viactx.session.- No subclassing.
StateMachine.from_dict(...)+await machine.run(...). No framework lifecycle to inherit into. - Every run is traceable. Each guard/action fired is recorded in order; a
run_id(yours, or auto-generated) ties every log line from one run together.
Development
uv sync
uv run python -m unittest discover -s tests
uv run coverage run -m unittest discover -s tests && uv run coverage report --fail-under=100
uv run ruff check tests statem
License
MIT — see LICENSE.
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 statem-0.2.0.tar.gz.
File metadata
- Download URL: statem-0.2.0.tar.gz
- Upload date:
- Size: 78.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
229e744b33e95a8368d0cd79dbe85d90ca885da53c9c3078d185b074b3d3a7eb
|
|
| MD5 |
6be9468ee9a42fe80a0e4f9020816347
|
|
| BLAKE2b-256 |
095f5326400ef3ccf9da63fe1065303304feae8709d1f1283e8d8461b6266f6f
|
Provenance
The following attestation bundles were made for statem-0.2.0.tar.gz:
Publisher:
publish.yml on RahulDas-dev/statem
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
statem-0.2.0.tar.gz -
Subject digest:
229e744b33e95a8368d0cd79dbe85d90ca885da53c9c3078d185b074b3d3a7eb - Sigstore transparency entry: 2311445003
- Sigstore integration time:
-
Permalink:
RahulDas-dev/statem@675a0471325503c8102bdfc9c61976ffde9cfb09 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/RahulDas-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@675a0471325503c8102bdfc9c61976ffde9cfb09 -
Trigger Event:
release
-
Statement type:
File details
Details for the file statem-0.2.0-py3-none-any.whl.
File metadata
- Download URL: statem-0.2.0-py3-none-any.whl
- Upload date:
- Size: 14.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a660707f72b10350a56e4c824c06b7e8fb906850806a189170cdcce54d792c05
|
|
| MD5 |
37762f2e611c9842ce205a70f13d0c08
|
|
| BLAKE2b-256 |
471e5b9ded7d99ad0d5c0cc4dd5368300ad08ce99565efa73c4a7ef2e76819f9
|
Provenance
The following attestation bundles were made for statem-0.2.0-py3-none-any.whl:
Publisher:
publish.yml on RahulDas-dev/statem
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
statem-0.2.0-py3-none-any.whl -
Subject digest:
a660707f72b10350a56e4c824c06b7e8fb906850806a189170cdcce54d792c05 - Sigstore transparency entry: 2311445013
- Sigstore integration time:
-
Permalink:
RahulDas-dev/statem@675a0471325503c8102bdfc9c61976ffde9cfb09 -
Branch / Tag:
refs/tags/v0.2.0 - Owner: https://github.com/RahulDas-dev
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@675a0471325503c8102bdfc9c61976ffde9cfb09 -
Trigger Event:
release
-
Statement type: