Python finite-state machines made easy.
- Free software: MIT license
- Documentation: https://python-statemachine.readthedocs.io.
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.
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)
...
... slowdown = green.to(yellow)
... stop = yellow.to(red)
... go = 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:
>>> traffic_light = TrafficLightMachine()
Then start sending events:
>>> traffic_light.cycle()
'Running cycle from green to yellow'
You can inspect the current state:
>>> traffic_light.current_state.id
'yellow'
A State
human-readable name is automatically derived from the State.id
:
>>> traffic_light.current_state.name
'Yellow'
Or get a complete state representation for debugging purposes:
>>> traffic_light.current_state
State('Yellow', id='yellow', value='yellow', initial=False, final=False)
The State
instance can also be checked by equality:
>>> traffic_light.current_state == TrafficLightMachine.yellow
True
>>> traffic_light.current_state == traffic_light.yellow
True
But for your convenience, can easily ask if a state is active at any time:
>>> traffic_light.green.is_active
False
>>> traffic_light.yellow.is_active
True
>>> traffic_light.red.is_active
False
Easily iterate over all states:
>>> [s.id for s in traffic_light.states]
['green', 'red', 'yellow']
Or over events:
>>> [t.name for t in traffic_light.events]
['cycle', 'go', 'slowdown', 'stop']
Call an event by its name:
>>> traffic_light.cycle()
Don't move.
'Running cycle from yellow to red'
Or send an event with the event name:
>>> traffic_light.send('cycle')
Go ahead!
'Running cycle from red to green'
>>> traffic_light.green.is_active
True
You can't run a transition from an invalid state:
>>> traffic_light.go()
Traceback (most recent call last):
statemachine.exceptions.TransitionNotAllowed: Can't go when in Green.
Keeping the same state as expected:
>>> traffic_light.green.is_active
True
And you can pass arbitrary positional or keyword arguments to the event, and they will be propagated to all actions and callbacks:
>>> traffic_light.cycle(message="Please, now slowdown.")
'Running cycle from green to yellow. Please, now slowdown.'
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.
-
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]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!