Library / SDK
glyph/automat avatar
glyph/automat

Automat: state machines your callers never have to know about

Self-service finite-state machines for the programmer on the go.

650 stars70 forksPythonMIT

At a glance

What is it?
Automat is a Python library for expressing finite state machines, particularly deterministic transducers, and its design principle is that the caller should not have to know a state machine is involved. You describe your inputs as a typing Protocol, declare states and transitions, and the machine you get back is an ordinary object with ordinary methods.
Who is it for?
Automat suits a Python developer whose object has several interacting flags that must stay consistent, and who wants the invalid states to be unrepresentable rather than merely unreachable. It does not suit a codebase that wants a configuration-driven state machine from a YAML file, and it does not suit a team that expects the states to be visible at runtime.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The coffee machine that cannot be in two states at once

The readme opens with a coffee machine, and the example is doing more than illustrating a state machine. It is making the argument for the library.

The scenario is an object whose behaviour varies with its state but which still presents a consistent interface. The machine has a lid that can be opened or closed, a chamber for water, a chamber for coffee beans, and a button for brew. The button should only actually brew in one of the possible configurations, and the open lid button should only work if the coffee is not, in fact, brewing.

The naive implementation keeps a collection of attributes consistent: hasWater, hasBeans, isLidOpen and so on. The problem is what happens as the machine grows. Add a chamber for flavourings to make hazelnut coffee and you have to keep adding more checks and more reasoning about which combinations of states are allowed.

The alternative is not to check the flags in every method. A state machine guarantees that if your code runs at all, it runs with all the required values initialised, because they have to be called in the order you declare them. The invalid combination is not merely unreachable, it is not representable.

That is the pitch, and it is a real one. The library is aimed at the case where the state is a property of the domain rather than of the control flow, and where the checks required to maintain it are the tedious part.

Callers get method calls, not input objects

The section titled what makes Automat different opens by conceding the crowded field. There are dozens of libraries on PyPI implementing state machines, so the author says why yet another one would be a good idea.

The principle is this: while organising your code around state machines is a good idea, your callers do not, and should not have to, care that you have done so. In Python the input to a stateful system is a method call, and the output may be a method call if you need to invoke a side effect, or a return value if you are just performing a computation in memory.

Most other state machine libraries require you to explicitly create an input object, provide it to a generic input method, and receive results, sometimes in terms of that library's interfaces and sometimes in terms of classes you define yourself.

That is the whole difference in one paragraph. The alternative design pushes the state machine into the caller's awareness: the caller constructs a Transition object, hands it over, and gets back a result object. Automat inverts that so the caller writes ordinary Python.

The consequence is that the state machine becomes an implementation detail of the class rather than a public protocol. A caller that was written before the refactor keeps working, because a method call is a method call. That is what makes the refactor to a state machine safe to do incrementally on an existing codebase.

Inputs are a Protocol, states are opaque tokens

The example is worth following in full, because each step is small and the shape of the library emerges from them.

The first step is a typing Protocol describing all of the inputs, which is what makes the machine type aware:

python
from typing import Protocol

class CoffeeBrewer(Protocol):
    def brewButton(self) -> None:
        "The user pressed the 'brew' button."
    def putInBeans(self) -> None:
        "The user put in some beans."

The second step is a concrete class holding the shared core of state, the parts that do not change between states:

python
from dataclasses import dataclass

@dataclass
class BrewerCore:
    heatingElement: HeatingElement

The third step declares the states, using a TypeMachineBuilder that takes the protocol and the core class:

python
from automat import TypeMachineBuilder

builder = TypeMachineBuilder(CoffeeBrewer, BrewerCore)
noBeans = builder.state("noBeans")
haveBeans = builder.state("haveBeans")

A transition with no behaviour is declared as a chain, and one with behaviour is declared with a decorator whose function receives the inputs and the core:

python
noBeans.upon(CoffeeBrewer.putInBeans).to(haveBeans).returns(None)
python
@haveBeans.upon(CoffeeBrewer.brewButton).to(noBeans)
def heatUp(inputs: CoffeeBrewer, core: BrewerCore) -> None:
    """
    When we have beans, upon pressing the brew button, we will then not have
    beans any more (as they have been entered into the brewing chamber) and
    our output will be heating the heating element.
    """
    print("Brewing the coffee...")
    core.heatingElement.turnOn()

Finally the machine is built, and building it gives a callable that takes a core and returns a synthetic implementation of the protocol:

python
newCoffeeMachine = builder.build()
python
>>> coffee = newCoffeeMachine(BrewerCore(HeatingElement()))
>>> machine.putInBeans()
>>> machine.brewButton()
Brewing the coffee...

All of the inputs are provided by calling them like methods, all of the output behaviours are invoked automatically when they are produced according to the outputs specified to upon, and all of the states are simply opaque tokens. That last property is what the name of the states library is about: the caller never sees them.

Where the state machine library earns its keep

The choice of library for this problem is not obvious, and the readme is right to address it directly.

There are dozens of state machine packages on PyPI, and they differ in what they consider the primary interface. Some are built around a declarative table where you list states and transitions in data, and the library generates the class for you. That approach is good when the machine is the whole design, and it is a poor fit when the machine is one part of a larger object with other responsibilities, because the generated class has to be composed with the rest somehow.

Automat takes the opposite approach. It is a library, not a generator, and it gives you a machine object rather than a class definition. That means the state logic lives inside a class you already have, the core is a small dataclass you own, and the protocol is an interface you can write tests against.

The second thing that makes it different is the emphasis on determinism. The documentation names deterministic finite-state transducers specifically, which is a term of art: a transducer is a machine that consumes a sequence of inputs and produces a sequence of outputs, and a deterministic one has exactly one possible output for each input in each state. A state machine that is not deterministic has to resolve ambiguity at runtime, and Automat makes that a declaration-time question instead, because you cannot declare two transitions from the same state on the same input without the library objecting.

For a reader deciding whether to look further, the practical test is whether your object has a set of flags that must agree with each other. If it does, that is the problem Automat was written for, and the coffee machine in the readme is a small version of it. If your object simply has a mode field and an if statement, this is more machinery than the problem needs.

A typed package with a visualisation extra

The packaging and the tooling are worth a short look, because this is a library whose author is also a Twisted maintainer and the surrounding details reflect that.

The project requires Python 3.9 or newer, with classifiers for 3.9 through 3.13, and it is marked as typed. The only runtime dependency is typing_extensions, and only on Python below 3.10, which is a good sign for a library that aims to be inserted into existing codebases rather than to pull in a stack.

The version is dynamic and comes from the version control system. The build system section lists setuptools, wheel, setuptools-scm and hatch-vcs as build requirements, and the configuration sets the setuptools_scm and hatch version source to vcs. The author is credited as Glyph with an email address at glyph.im, which is the same Glyph who writes Twisted.

There is an optional extra for visualisation, pulling in graphviz above version 0.5.1 and Twisted above 16.1.1, and it installs a console script called automat-visualize. Being able to draw the state machine is genuinely useful for a library whose selling point is that the states are invisible to callers, since a picture puts them back in view for a code review.

The repository layout is conventional for a modern Python package: src/ for the code, tests/ is not at the top level but the configuration points the package finder at src, and there is a docs/ directory, a benchmark/ directory, a typical_example_happy.py file at the root, a tox.ini, a mypy.ini and a .pydoctor.cfg for API documentation generation, plus a .coveragerc.

The maintenance position is modest but real. The repository is not archived, the default branch is trunk rather than master, and the last push was on 2026-08-29, with no GitHub releases. The licence is MIT, with the LICENSE file at the root, and there is a SECURITY.md alongside.

Editorial conclusion

Automat suits a Python developer whose object has several interacting flags that must stay consistent, and who wants the invalid states to be unrepresentable rather than merely unreachable. It does not suit a codebase that wants a configuration-driven state machine from a YAML file, and it does not suit a team that expects the states to be visible at runtime. Before adopting it, read the example in the readme end to end, and note the packaging detail that Automat is versioned from the git tag with setuptools-scm and hatch-vcs, so a checkout without tags produces a development version.

Frequently asked questions

What is Automat?

It is a library for concise, idiomatic Python expression of finite-state automata, particularly deterministic finite-state transducers. The design principle is that while organising your code around state machines is a good idea, your callers do not and should not have to care that you have done so.

How do I define the states and transitions in Automat?

You create a TypeMachineBuilder with a typing Protocol describing your inputs and a core class holding shared state, declare states with builder.state, and describe transitions with the upon method, optionally decorated with a function that receives the inputs and the core. builder.build() then returns a callable that takes a core and produces the synthetic implementation.

How does Automat differ from other Python state machine libraries?

Most other libraries require you to create an input object, pass it to a generic input method, and receive results in the library's terms. In Automat the inputs are provided by calling them like ordinary methods, the output behaviours are invoked automatically, and the states are opaque tokens the caller never sees.

What are the dependencies and requirements of Automat?

The only runtime dependency is typing_extensions, and only on Python below 3.10, with Python 3.9 or newer required. There is an optional visualize extra with graphviz and Twisted, which installs the automat-visualize console script for drawing the state machine.

Official sources

  1. glyph/automat on GitHub
  2. Issues
  3. License: MIT
  4. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/glyph-automat.svg)](https://hysenlabs.com/projects/glyph-automat)

Was this useful?