Python Finite State Machines made easy.
A library for building finite state machines in Python, with support for sync and async engines, Django integration, diagram generation, and a flexible callback/listener system.
- Source code:
statemachine/ - Tests:
tests/ - Documentation:
docs/(Sphinx + MyST Markdown, hosted on ReadTheDocs)
statemachine.py— CoreStateMachineclassfactory.py—StateMachineMetaclasshandles class construction, state/transition validationstate.py/event.py— Descriptor-basedStateandEventdefinitionstransition.py/transition_list.py— Transition logic and composition (|operator)callbacks.py— Priority-based callback registry (CallbackPriority,CallbackGroup)dispatcher.py— Listener/observer pattern,callable_methodwraps callables with signature adaptationsignature.py—SignatureAdapterfor dependency injection into callbacksengines/sync.py,engines/async_.py— Sync and async run-to-completion enginesregistry.py— Global state machine registry (used byMachineMixin)mixins.py—MachineMixinfor domain model integration (e.g., Django models)spec_parser.py— Boolean expression parser for condition guardscontrib/diagram.py— Diagram generation via pydot/Graphviz
uv sync --all-extras --dev
pre-commit installAlways use uv to run commands:
# Run all tests (parallel)
uv run pytest -n auto
# Run a specific test file
uv run pytest tests/test_signature.py
# Run a specific test
uv run pytest tests/test_signature.py::TestSignatureAdapter::test_wrap_fn_single_positional_parameter
# Skip slow tests
uv run pytest -m "not slow"When trying to run all tests, prefer to use xdist (-n) as some SCXML tests uses timeout of 30s to verify fallback mechanism.
Don't specify the directory tests/, because this will exclude doctests from both source modules (--doctest-modules) and markdown docs
(--doctest-glob=*.md) (enabled by default):
uv run pytest -n autoCoverage is enabled by default.
# Lint
uv run ruff check .
# Auto-fix lint issues
uv run ruff check --fix .
# Format
uv run ruff format .
# Type check
uv run mypy statemachine/ tests/- Formatter/Linter: ruff (line length 99, target Python 3.9)
- Rules: pycodestyle, pyflakes, isort, pyupgrade, flake8-comprehensions, flake8-bugbear, flake8-pytest-style
- Imports: single-line, sorted by isort
- Docstrings: Google convention
- Naming: PascalCase for classes, snake_case for functions/methods, UPPER_SNAKE_CASE for constants
- Type hints: used throughout;
TYPE_CHECKINGfor circular imports - Pre-commit hooks enforce ruff + mypy + pytest
- Decouple infrastructure from domain: Modules like
signature.pyanddispatcher.pyare general-purpose (signature adaptation, listener/observer pattern) and intentionally not coupled to the state machine domain. Prefer this separation even for modules that are only used internally — it keeps responsibilities clear and the code easier to reason about. - Favor small, focused modules: When adding new functionality, consider whether it can live in its own module with a well-defined boundary, rather than growing an existing one.
# Build HTML docs
uv run sphinx-build docs docs/_build/html
# Live reload for development
uv run sphinx-autobuild docs docs/_build/html --re-ignore "auto_examples/.*"- Main branch:
develop - PRs target
develop - Releases are tagged as
v*.*.* - Signed commits preferred (
git commit -s) - Use Conventional Commits messages
(e.g.,
feat:,fix:,refactor:,test:,docs:,chore:,perf:)
- Do not commit secrets, credentials, or
.envfiles - Validate at system boundaries; trust internal code