Skip to main content

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.

CI Docs PyPI Python Versions Coverage License: MIT

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. config is 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.
  • session is yours. Any shape you want (dict, dataclass, ORM row) — the engine never inspects it, just threads it through to your guards/actions via ctx.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

statem-0.2.0.tar.gz (78.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

statem-0.2.0-py3-none-any.whl (14.3 kB view details)

Uploaded Python 3

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

Hashes for statem-0.2.0.tar.gz
Algorithm Hash digest
SHA256 229e744b33e95a8368d0cd79dbe85d90ca885da53c9c3078d185b074b3d3a7eb
MD5 6be9468ee9a42fe80a0e4f9020816347
BLAKE2b-256 095f5326400ef3ccf9da63fe1065303304feae8709d1f1283e8d8461b6266f6f

See more details on using hashes here.

Provenance

The following attestation bundles were made for statem-0.2.0.tar.gz:

Publisher: publish.yml on RahulDas-dev/statem

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for statem-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a660707f72b10350a56e4c824c06b7e8fb906850806a189170cdcce54d792c05
MD5 37762f2e611c9842ce205a70f13d0c08
BLAKE2b-256 471e5b9ded7d99ad0d5c0cc4dd5368300ad08ce99565efa73c4a7ef2e76819f9

See more details on using hashes here.

Provenance

The following attestation bundles were made for statem-0.2.0-py3-none-any.whl:

Publisher: publish.yml on RahulDas-dev/statem

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page