Skip to main content

Python Finite State Machines made easy.

Project description

Python StateMachine

pypi downloads build status Coverage report Documentation Status GitHub commits since last release (main)

Python finite-state machines made easy.

Welcome to python-statemachine, an intuitive and powerful state machine framework designed for a great developer experience.

🚀 With StateMachine, you can easily create complex, dynamic systems with clean, readable code.

💡 Our framework makes it easy to understand and reason about the different states, events and transitions in your system, so you can focus on building great products.

🔒 python-statemachine also provides robust error handling and ensures that your system stays in a valid state at all times.

A few reasons why you may consider using it:

  • 📈 python-statemachine is designed to help you build scalable, maintainable systems that can handle any complexity.
  • 💪 You can easily create and manage multiple state machines within a single application.
  • 🚫 Prevents common mistakes and ensures that your system stays in a valid state at all times.

Getting started

To install Python State Machine, run this command in your terminal:

pip install python-statemachine

To generate diagrams from your machines, you'll also need pydot and Graphviz. You can install this library already with pydot dependency using the extras install option. See our docs for more details.

pip install python-statemachine[diagrams]

Define your state machine:

>>> from statemachine import StateMachine, State

>>> class TrafficLightMachine(StateMachine):
...     "A traffic light machine"
...     green = State(initial=True)
...     yellow = State()
...     red = State()
...
...     cycle = (
...         green.to(yellow)
...         | yellow.to(red)
...         | red.to(green)
...     )
...
...     def before_cycle(self, event: str, source: State, target: State, message: str = ""):
...         message = ". " + message if message else ""
...         return f"Running {event} from {source.id} to {target.id}{message}"
...
...     def on_enter_red(self):
...         print("Don't move.")
...
...     def on_exit_red(self):
...         print("Go ahead!")

You can now create an instance:

>>> sm = TrafficLightMachine()

This state machine can be represented graphically as follows:

>>> img_path = "docs/images/readme_trafficlightmachine.png"
>>> sm._graph().write_png(img_path)

Where on the TrafficLightMachine, we've defined green, yellow, and red as states, and one event called cycle, which is bound to the transitions from green to yellow, yellow to red, and red to green. We also have defined three callbacks by name convention, before_cycle, on_enter_red, and on_exit_red.

Then start sending events to your new state machine:

>>> sm.send("cycle")
'Running cycle from green to yellow'

That's it. This is all an external object needs to know about your state machine: How to send events. Ideally, all states, transitions, and actions should be kept internally and not checked externally to avoid unnecessary coupling.

But if your use case needs, you can inspect state machine properties, like the current state:

>>> sm.current_state.id
'yellow'

Or get a complete state representation for debugging purposes:

>>> sm.current_state
State('Yellow', id='yellow', value='yellow', initial=False, final=False)

The State instance can also be checked by equality:

>>> sm.current_state == TrafficLightMachine.yellow
True

>>> sm.current_state == sm.yellow
True

Or you can check if a state is active at any time:

>>> sm.green.is_active
False

>>> sm.yellow.is_active
True

>>> sm.red.is_active
False

Easily iterate over all states:

>>> [s.id for s in sm.states]
['green', 'red', 'yellow']

Or over events:

>>> [t.name for t in sm.events]
['cycle']

Call an event by its name:

>>> sm.cycle()
Don't move.
'Running cycle from yellow to red'

Or send an event with the event name:

>>> sm.send('cycle')
Go ahead!
'Running cycle from red to green'

>>> sm.green.is_active
True

You can pass arbitrary positional or keyword arguments to the event, and they will be propagated to all actions and callbacks using something similar to dependency injection. In other words, the library will only inject the parameters declared on the callback method.

Note how before_cycle was declared:

def before_cycle(self, event: str, source: State, target: State, message: str = ""):
    message = ". " + message if message else ""
    return f"Running {event} from {source.id} to {target.id}{message}"

The params event, source, target (and others) are available built-in to be used on any action. The param message is user-defined, in our example we made it default empty so we can call cycle with or without a message parameter.

If we pass a message parameter, it will be used on the before_cycle action:

>>> sm.send("cycle", message="Please, now slowdown.")
'Running cycle from green to yellow. Please, now slowdown.'

By default, events with transitions that cannot run from the current state or unknown events raise a TransitionNotAllowed exception:

>>> sm.send("go")
Traceback (most recent call last):
statemachine.exceptions.TransitionNotAllowed: Can't go when in Yellow.

Keeping the same state as expected:

>>> sm.yellow.is_active
True

A human-readable name is automatically derived from the State.id, which is used on the messages and in diagrams:

>>> sm.current_state.name
'Yellow'

A more useful example

A simple didactic state machine for controlling an Order:

>>> class OrderControl(StateMachine):
...     waiting_for_payment = State(initial=True)
...     processing = State()
...     shipping = State()
...     completed = State(final=True)
...
...     add_to_order = waiting_for_payment.to(waiting_for_payment)
...     receive_payment = (
...         waiting_for_payment.to(processing, cond="payments_enough")
...         | waiting_for_payment.to(waiting_for_payment, unless="payments_enough")
...     )
...     process_order = processing.to(shipping, cond="payment_received")
...     ship_order = shipping.to(completed)
...
...     def __init__(self):
...         self.order_total = 0
...         self.payments = []
...         self.payment_received = False
...         super(OrderControl, self).__init__()
...
...     def payments_enough(self, amount):
...         return sum(self.payments) + amount >= self.order_total
...
...     def before_add_to_order(self, amount):
...         self.order_total += amount
...         return self.order_total
...
...     def before_receive_payment(self, amount):
...         self.payments.append(amount)
...         return self.payments
...
...     def after_receive_payment(self):
...         self.payment_received = True
...
...     def on_enter_waiting_for_payment(self):
...         self.payment_received = False

You can use this machine as follows.

>>> control = OrderControl()

>>> control.add_to_order(3)
3

>>> control.add_to_order(7)
10

>>> control.receive_payment(4)
[4]

>>> control.current_state.id
'waiting_for_payment'

>>> control.current_state.name
'Waiting for payment'

>>> control.process_order()
Traceback (most recent call last):
...
statemachine.exceptions.TransitionNotAllowed: Can't process_order when in Waiting for payment.

>>> control.receive_payment(6)
[4, 6]

>>> control.current_state.id
'processing'

>>> control.process_order()

>>> control.ship_order()

>>> control.payment_received
True

>>> control.order_total
10

>>> control.payments
[4, 6]

>>> control.completed.is_active
True

There's a lot more to cover, please take a look at our docs: https://python-statemachine.readthedocs.io.

Contributing to the project

  • If you found this project helpful, please consider giving it a star on GitHub.

  • Contribute code: If you would like to contribute code to this project, please submit a pull request. For more information on how to contribute, please see our contributing.md file.

  • Report bugs: If you find any bugs in this project, please report them by opening an issue on our GitHub issue tracker.

  • Suggest features: If you have a great idea for a new feature, please let us know by opening an issue on our GitHub issue tracker.

  • Documentation: Help improve this project's documentation by submitting pull requests.

  • Promote the project: Help spread the word about this project by sharing it on social media, writing a blog post, or giving a talk about it. Tag me on Twitter @fgmacedo so I can share it too!

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

python_statemachine-2.1.2.tar.gz (31.5 kB view details)

Uploaded Source

Built Distribution

python_statemachine-2.1.2-py3-none-any.whl (35.7 kB view details)

Uploaded Python 3

File details

Details for the file python_statemachine-2.1.2.tar.gz.

File metadata

  • Download URL: python_statemachine-2.1.2.tar.gz
  • Upload date:
  • Size: 31.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.5.1 CPython/3.11.1 Linux/5.15.123.1-microsoft-standard-WSL2

File hashes

Hashes for python_statemachine-2.1.2.tar.gz
Algorithm Hash digest
SHA256 0b0dd8b28738b53f14391b06d5072cd5e72259da5ae23574d3d4f5e6dd366663
MD5 a11a5a45779ca52f7449c44c3f314ad6
BLAKE2b-256 c5f0c842cde8ac1998235f9d58be428d1705610e7bf0d983763c15f0f4247dc3

See more details on using hashes here.

File details

Details for the file python_statemachine-2.1.2-py3-none-any.whl.

File metadata

  • Download URL: python_statemachine-2.1.2-py3-none-any.whl
  • Upload date:
  • Size: 35.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.5.1 CPython/3.11.1 Linux/5.15.123.1-microsoft-standard-WSL2

File hashes

Hashes for python_statemachine-2.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 d7e369d5da5b9007cc7cf5eb7a1b169081e2f4b7d30b6415fc122858fb7696ec
MD5 ddb3831166336ce429c560c662225331
BLAKE2b-256 35b5249dc0f5bc640bf009891fbc013072735bc6ed5d2e83503d304a3c1685e7

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page