# python-statemachine: flat state machines and full statecharts in one Python library

> python-statemachine lets you declare states as class attributes, wire transitions as expressions, and add compound states, parallel regions and history pseudo-states when a workflow outgrows a boolean flag. It runs the same code in sync and async codebases, MIT licensed, and is at version 3.2.2.

**fgmacedo/python-statemachine** — Expressive statecharts and FSMs for modern Python.

- Repository: https://github.com/fgmacedo/python-statemachine
- Website: https://python-statemachine.readthedocs.io
- Stars: 1,327 · Forks: 116
- Language: Python
- License: MIT
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/fgmacedo-python-statemachine

## States as class attributes, transitions as expressions

The library describes itself as expressive statecharts and FSMs for modern Python, with a clean, declarative API that works in both sync and async codebases. The quick start shows what that means in practice. A class subclasses `StateChart`, its states are class attributes, and the transitions are an expression built with the `to` operator and combined with the pipe character:

```py
>>> class TrafficLightMachine(StateChart):
...     "A traffic light machine"
...     green = State(initial=True)
...     yellow = State()
...     red = State()
...
...     cycle = (
...         green.to(yellow)
...         | yellow.to(red)
...         | red.to(green)
...     )
```

Callbacks attach by naming convention, `before_cycle`, `on_enter_red`, `on_exit_red`, and the README makes a point of the ergonomics here: parameters are injected into callbacks automatically, because the library inspects the signature and provides only the arguments each callback needs. That is a small feature with a large effect on how the code reads. A callback that only needs the target state does not have to accept the event and the source as well, and a callback that needs a score can declare it and receive it, as the guard example does with `score: int = 0`. You then instantiate the class and send events, and the machine decides what is legal.

## Guards, and why declaration order is part of the semantics

Transitions are filtered by guards using `cond=` and `unless=`. The example in the README is an approval workflow with two transitions on the same event, one guarded and one not:

```py
>>> class ApprovalWorkflow(StateChart):
...     pending = State(initial=True)
...     approved = State(final=True)
...     rejected = State(final=True)
...
...     review = (
...         pending.to(approved, cond="is_valid")
...         | pending.to(rejected)
...     )
...
...     def is_valid(self, score: int = 0):
...         return score >= 70
```

The rule the README states is that when multiple transitions share the same event, declaration order determines priority, and that the first transition whose guard passes wins. In the example, sending `review` with a score of 50 leaves the machine in `rejected`, because `is_valid` returns false and the second transition, which has no guard and therefore always matches, fires instead. With a score of 85 the machine ends in `approved`. This has a consequence worth naming before you adopt the pattern. Priority is positional, so a reader has to scan the whole transition list to know what a given event does, and reordering the list silently changes behaviour. It is predictable, which is what matters, but it is not the self documenting rule table that some other modelling tools give you.

## Compound states, parallel regions and history, the statechart part

This is where the library goes beyond a flat finite state machine. A compound state is declared with `State.Compound`, and the README states the two rules that matter: entering a compound activates both the parent and its `initial` child, and exiting removes the parent and all its descendants. A document workflow with an `editing` compound containing `draft` and `review` therefore reports `editing` and `draft` as active at the same time, and after `approve` the machine reports only `published`. Parallel states use `State.Parallel` to activate all child regions simultaneously, and events in one region do not affect the others, which is how the deploy pipeline example runs `compiling` and `running` at the same time. The completion rule is specific and useful: a `done.state` event fires only when all regions reach a final state, so `done_state_deploy` does not fire after the build finishes but after the build and the tests have both finished. History states, created with `HistoryState()`, record which child was active when a compound was exited, so re-entering through the history pseudo-state restores the previous child instead of starting from the initial one. The README shows an editor that returns to the `visual` tab rather than to `source`.

## Reading the machine back: configuration, markdown tables and diagrams

A state machine is only useful if you can inspect it at runtime and explain it to a colleague, and this library offers three ways. At runtime, `sm.configuration` returns an ordered set of the active states, `sm.green.is_active` answers the single state question directly, and `sm.configuration_values` gives the set of active state identifiers, which is what the compound and parallel examples assert on. For a human, an f-string with the `md` format specifier prints a markdown table of the machine's transitions, with columns for State, Event, Guard and Target, which is the kind of table you can paste into a pull request description. For a picture, the graph is available and can be written out as a PNG, and the optional `diagrams` extra, which pulls in pydot, exists for that. The `State` and `StateChart` objects are inspectable, which also means the machine is testable as data rather than only through its transitions, and the repository has an extensive `tests/` directory and a `conftest.py` at the root, with pytest, pytest-cov, pytest-asyncio, pytest-benchmark and pytest-mock in the development dependency group.

## Packaging: optional extras for diagrams, YAML, validation and IO

The project metadata describes the supported surface precisely. The package is `python-statemachine`, version 3.2.2, it requires Python 3.10 or later, and the classifiers list Python 3.10 through 3.15, a development status of Production/Stable, and two framework classifiers, one for AsyncIO and one for Django, which are the two integration paths the project cares about. The dependency list is deliberately empty by default, and everything optional is an extra. `diagrams` brings in pydot for graph output, `yaml` brings in PyYAML, `validation` brings in jsonschema, and `io` combines PyYAML and jsonschema, which reads as a feature to load and save machines from a document with a schema attached. The development group is where the project's own standards live: ruff for linting and formatting, mypy for typing, pre-commit for hooks, Sphinx with several extensions for the documentation, pillow for image output, and Django pinned to different versions depending on the Python version. The repository also carries a `.pre-commit-config.yaml`, an `AGENTS.md` and a `CLAUDE.md`, and the documentation is published on Read the Docs.

## What the library does not do for you

Two boundaries are worth being explicit about. The first is persistence. The machine is an object in your process, and the examples construct it and send events to it, so if the workflow spans a request, a job queue and a restart, storing the active configuration and restoring it is your design work. A library that owns the transitions but not the storage gives you clean rules and no durability, and teams that conflate the two end up with a state machine that can legally be in a state the database does not know about. The second is that this is a modelling library, not an orchestration tool. It does not run your code on a schedule, it does not retry, and it does not give you a durable execution model; it tells you which transition is allowed from here, and calls your callbacks. If what you need is a workflow engine with persistence, retries and timers, this is the wrong layer, and if what you need is to replace a cluster of booleans with something a newcomer can read, it is exactly the right one.

## Conclusion

Adopt python-statemachine when a workflow in your service has become a set of booleans and flags that nobody can read, and when the transitions are business rules worth reading in one class definition. Do not adopt it for a two state toggle, where the ceremony costs more than the bug, and do not expect it to persist state for you, because the machine is an object in your process and durability is your design decision. Before you commit, decide whether you need flat states or the statechart features, since compound, parallel and history states are where the complexity and the value both live, and check the guard semantics against your expectation, because declaration order decides which transition wins. The licence is MIT, the current release is 3.2.2 from 2026-10-14, and the last push was on 2026-10-15.

## FAQ

### How do I define a state machine in python-statemachine?

Subclass `StateChart`, declare states as class attributes such as `green = State(initial=True)`, and express transitions as an expression like `green.to(yellow) | yellow.to(red)`. Callbacks attach by naming convention, with `before_`, `on_enter_` and `on_exit_` prefixes, and their parameters are injected automatically based on the signature.

### How do guards work when several transitions share an event?

Guards are written with `cond=` and `unless=`. When multiple transitions share the same event, declaration order determines priority and the first transition whose guard passes wins, so with a guarded transition listed first and an unguarded one second, a false guard falls through to the unguarded transition.

### Does python-statemachine support parallel and history states?

Yes. `State.Parallel` activates all child regions at once, and a `done.state` event fires only when all regions reach a final state. `HistoryState()` records which child was active when a compound was exited, so re-entering through the history pseudo-state restores the previous child instead of the initial one.

### Is python-statemachine usable with async code?

The README says the declarative API works in both sync and async codebases, and the project metadata lists a Framework :: AsyncIO classifier alongside a Framework :: Django one, with pytest-asyncio in the development dependencies.

### What optional dependencies does python-statemachine have?

The `diagrams` extra installs pydot for graph output, `yaml` installs PyYAML, `validation` installs jsonschema, and `io` installs both PyYAML and jsonschema. The core package has no runtime dependencies, and Python 3.10 or later is required.

## Sources

- [fgmacedo/python-statemachine on GitHub](https://github.com/fgmacedo/python-statemachine)
- [License: MIT](https://github.com/fgmacedo/python-statemachine/blob/main/LICENSE)
- [Project website](https://python-statemachine.readthedocs.io)
- [README](https://github.com/fgmacedo/python-statemachine/blob/main/README.md)
- [Releases](https://github.com/fgmacedo/python-statemachine/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/fgmacedo-python-statemachine
