{"id":13494833,"url":"https://github.com/fgmacedo/python-statemachine","last_synced_at":"2025-05-13T22:04:56.108Z","repository":{"id":42982332,"uuid":"85780522","full_name":"fgmacedo/python-statemachine","owner":"fgmacedo","description":"Python Finite State Machines made easy.","archived":false,"fork":false,"pushed_at":"2025-04-14T21:30:38.000Z","size":2306,"stargazers_count":1036,"open_issues_count":10,"forks_count":93,"subscribers_count":19,"default_branch":"develop","last_synced_at":"2025-04-30T14:15:00.099Z","etag":null,"topics":["automata","finite-state-machine","fsm","fsm-library","python","state","state-machine","state-machine-diagram","state-machine-dsl","state-management","statemachine"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/fgmacedo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"contributing.md","funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null},"funding":{"github":"fgmacedo"}},"created_at":"2017-03-22T03:24:42.000Z","updated_at":"2025-04-30T10:53:09.000Z","dependencies_parsed_at":"2023-10-04T04:52:24.672Z","dependency_job_id":"d5799bbb-43e2-4f9d-a370-a92096ab786d","html_url":"https://github.com/fgmacedo/python-statemachine","commit_stats":{"total_commits":230,"total_committers":12,"mean_commits":"19.166666666666668","dds":"0.21739130434782605","last_synced_commit":"2420b18c2907f6c86b6b1e3605e7d7957e9a8d38"},"previous_names":[],"tags_count":38,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fgmacedo%2Fpython-statemachine","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fgmacedo%2Fpython-statemachine/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fgmacedo%2Fpython-statemachine/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/fgmacedo%2Fpython-statemachine/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/fgmacedo","download_url":"https://codeload.github.com/fgmacedo/python-statemachine/tar.gz/refs/heads/develop","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":254036816,"owners_count":22003653,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["automata","finite-state-machine","fsm","fsm-library","python","state","state-machine","state-machine-diagram","state-machine-dsl","state-management","statemachine"],"created_at":"2024-07-31T19:01:28.692Z","updated_at":"2025-05-13T22:04:56.045Z","avatar_url":"https://github.com/fgmacedo.png","language":"Python","funding_links":["https://github.com/sponsors/fgmacedo"],"categories":["Python","Community","Libraries"],"sub_categories":["Python"],"readme":"# Python StateMachine\n\n[![pypi](https://img.shields.io/pypi/v/python-statemachine.svg)](https://pypi.python.org/pypi/python-statemachine)\n[![downloads total](https://static.pepy.tech/badge/python-statemachine)](https://pepy.tech/project/python-statemachine)\n[![downloads](https://img.shields.io/pypi/dm/python-statemachine.svg)](https://pypi.python.org/pypi/python-statemachine)\n[![Coverage report](https://codecov.io/gh/fgmacedo/python-statemachine/branch/develop/graph/badge.svg)](https://codecov.io/gh/fgmacedo/python-statemachine)\n[![Documentation Status](https://readthedocs.org/projects/python-statemachine/badge/?version=latest)](https://python-statemachine.readthedocs.io/en/latest/?badge=latest)\n[![GitHub commits since last release (main)](https://img.shields.io/github/commits-since/fgmacedo/python-statemachine/main/develop)](https://github.com/fgmacedo/python-statemachine/compare/main...develop)\n\n\nPython [finite-state machines](https://en.wikipedia.org/wiki/Finite-state_machine) made easy.\n\n\u003cdiv align=\"center\"\u003e\n\n![](https://github.com/fgmacedo/python-statemachine/blob/develop/docs/images/python-statemachine.png?raw=true)\n\n\u003c/div\u003e\n\nWelcome to python-statemachine, an intuitive and powerful state machine library designed for a\ngreat developer experience. We provide a _pythonic_ and expressive API for implementing state\nmachines in sync or asynchonous Python codebases.\n\n## Features\n\n- ✨ **Basic components**: Easily define **States**, **Events**, and **Transitions** to model your logic.\n- ⚙️ **Actions and handlers**: Attach actions and handlers to states, events, and transitions to control behavior dynamically.\n- 🛡️ **Conditional transitions**: Implement **Guards** and **Validators** to conditionally control transitions, ensuring they only occur when specific conditions are met.\n- 🚀 **Full async support**: Enjoy full asynchronous support. Await events, and dispatch callbacks asynchronously for seamless integration with async codebases.\n- 🔄 **Full sync support**: Use the same state machine from synchronous codebases without any modifications.\n- 🎨 **Declarative and simple API**: Utilize a clean, elegant, and readable API to define your state machine, making it easy to maintain and understand.\n- 👀 **Observer pattern support**: Register external and generic objects to watch events and register callbacks.\n- 🔍 **Decoupled design**: Separate concerns with a decoupled \"state machine\" and \"model\" design, promoting cleaner architecture and easier maintenance.\n- ✅ **Correctness guarantees**: Ensured correctness with validations at class definition time:\n   - Ensures exactly one `initial` state.\n   - Disallows transitions from `final` states.\n   - Requires ongoing transitions for all non-final states.\n   - Guarantees all non-final states have at least one path to a final state if final states are declared.\n   - Validates the state machine graph representation has a single component.\n- 📦 **Flexible event dispatching**: Dispatch events with any extra data, making it available to all callbacks, including actions and guards.\n- 🔧 **Dependency injection**: Needed parameters are injected into callbacks.\n- 📊 **Graphical representation**: Generate and output graphical representations of state machines. Create diagrams from the command line, at runtime, or even in Jupyter notebooks.\n- 🌍 **Internationalization support**: Provides error messages in different languages, making the library accessible to a global audience.\n- 🛡️ **Robust testing**: Ensured reliability with a codebase that is 100% covered by automated tests, including all docs examples. Releases follow semantic versioning for predictable releases.\n- 🏛️ **Domain model integration**: Seamlessly integrate with domain models using Mixins.\n- 🔧 **Django integration**: Automatically discover state machines in Django applications.\n\n\n\n## Installing\n\nTo install Python State Machine, run this command in your terminal:\n\n    pip install python-statemachine\n\nTo generate diagrams from your machines, you'll also need `pydot` and `Graphviz`. You can\ninstall this library already with `pydot` dependency using the `extras` install option. See\nour docs for more details.\n\n    pip install python-statemachine[diagrams]\n\n## First example\n\nDefine your state machine:\n\n```py\n\u003e\u003e\u003e from statemachine import StateMachine, State\n\n\u003e\u003e\u003e class TrafficLightMachine(StateMachine):\n...     \"A traffic light machine\"\n...     green = State(initial=True)\n...     yellow = State()\n...     red = State()\n...\n...     cycle = (\n...         green.to(yellow)\n...         | yellow.to(red)\n...         | red.to(green)\n...     )\n...\n...     def before_cycle(self, event: str, source: State, target: State, message: str = \"\"):\n...         message = \". \" + message if message else \"\"\n...         return f\"Running {event} from {source.id} to {target.id}{message}\"\n...\n...     def on_enter_red(self):\n...         print(\"Don't move.\")\n...\n...     def on_exit_red(self):\n...         print(\"Go ahead!\")\n\n```\n\nYou can now create an instance:\n\n```py\n\u003e\u003e\u003e sm = TrafficLightMachine()\n\n```\n\nThis state machine can be represented graphically as follows:\n\n```py\n\u003e\u003e\u003e # This example will only run on automated tests if dot is present\n\u003e\u003e\u003e getfixture(\"requires_dot_installed\")\n\u003e\u003e\u003e img_path = \"docs/images/readme_trafficlightmachine.png\"\n\u003e\u003e\u003e sm._graph().write_png(img_path)\n\n```\n\n![](https://raw.githubusercontent.com/fgmacedo/python-statemachine/develop/docs/images/readme_trafficlightmachine.png)\n\n\nWhere on the `TrafficLightMachine`, we've defined `green`, `yellow`, and `red` as states, and\none event called `cycle`, which is bound to the transitions from `green` to `yellow`, `yellow` to `red`,\nand `red` to `green`. We also have defined three callbacks by name convention, `before_cycle`, `on_enter_red`, and `on_exit_red`.\n\n\nThen start sending events to your new state machine:\n\n```py\n\u003e\u003e\u003e sm.send(\"cycle\")\n'Running cycle from green to yellow'\n\n```\n\n**That's it.** This is all an external object needs to know about your state machine: How to send events.\nIdeally, all states, transitions, and actions should be kept internally and not checked externally to avoid unnecessary coupling.\n\nBut if your use case needs, you can inspect state machine properties, like the current state:\n\n```py\n\u003e\u003e\u003e sm.current_state.id\n'yellow'\n\n```\n\nOr get a complete state representation for debugging purposes:\n\n```py\n\u003e\u003e\u003e sm.current_state\nState('Yellow', id='yellow', value='yellow', initial=False, final=False)\n\n```\n\nThe `State` instance can also be checked by equality:\n\n```py\n\u003e\u003e\u003e sm.current_state == TrafficLightMachine.yellow\nTrue\n\n\u003e\u003e\u003e sm.current_state == sm.yellow\nTrue\n\n```\n\nOr you can check if a state is active at any time:\n\n```py\n\u003e\u003e\u003e sm.green.is_active\nFalse\n\n\u003e\u003e\u003e sm.yellow.is_active\nTrue\n\n\u003e\u003e\u003e sm.red.is_active\nFalse\n\n```\n\nEasily iterate over all states:\n\n```py\n\u003e\u003e\u003e [s.id for s in sm.states]\n['green', 'yellow', 'red']\n\n```\n\nOr over events:\n\n```py\n\u003e\u003e\u003e [t.id for t in sm.events]\n['cycle']\n\n```\n\nCall an event by its id:\n\n```py\n\u003e\u003e\u003e sm.cycle()\nDon't move.\n'Running cycle from yellow to red'\n\n```\nOr send an event with the event id:\n\n```py\n\u003e\u003e\u003e sm.send('cycle')\nGo ahead!\n'Running cycle from red to green'\n\n\u003e\u003e\u003e sm.green.is_active\nTrue\n\n```\n\nYou can pass arbitrary positional or keyword arguments to the event, and\nthey 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\ncallback method.\n\nNote how `before_cycle` was declared:\n\n```py\ndef before_cycle(self, event: str, source: State, target: State, message: str = \"\"):\n    message = \". \" + message if message else \"\"\n    return f\"Running {event} from {source.id} to {target.id}{message}\"\n```\n\nThe params `event`, `source`, `target` (and others) are available built-in to be used on any action.\nThe param `message` is user-defined, in our example we made it default empty so we can call `cycle` with\nor without a `message` parameter.\n\nIf we pass a `message` parameter, it will be used on the `before_cycle` action:\n\n```py\n\u003e\u003e\u003e sm.send(\"cycle\", message=\"Please, now slowdown.\")\n'Running cycle from green to yellow. Please, now slowdown.'\n\n```\n\n\nBy default, events with transitions that cannot run from the current state or unknown events\nraise a `TransitionNotAllowed` exception:\n\n```py\n\u003e\u003e\u003e sm.send(\"go\")\nTraceback (most recent call last):\nstatemachine.exceptions.TransitionNotAllowed: Can't go when in Yellow.\n\n```\n\nKeeping the same state as expected:\n\n```py\n\u003e\u003e\u003e sm.yellow.is_active\nTrue\n\n```\n\nA human-readable name is automatically derived from the `State.id`, which is used on the messages\nand in diagrams:\n\n```py\n\u003e\u003e\u003e sm.current_state.name\n'Yellow'\n\n```\n\n## Async support\n\nWe support native coroutine using `asyncio`, enabling seamless integration with asynchronous code.\nThere's no change on the public API of the library to work on async codebases.\n\n\n```py\n\u003e\u003e\u003e class AsyncStateMachine(StateMachine):\n...     initial = State('Initial', initial=True)\n...     final = State('Final', final=True)\n...\n...     advance = initial.to(final)\n...\n...     async def on_advance(self):\n...         return 42\n\n\u003e\u003e\u003e async def run_sm():\n...     sm = AsyncStateMachine()\n...     result = await sm.advance()\n...     print(f\"Result is {result}\")\n...     print(sm.current_state)\n\n\u003e\u003e\u003e asyncio.run(run_sm())\nResult is 42\nFinal\n\n```\n\n## A more useful example\n\nA simple didactic state machine for controlling an `Order`:\n\n```py\n\u003e\u003e\u003e class OrderControl(StateMachine):\n...     waiting_for_payment = State(initial=True)\n...     processing = State()\n...     shipping = State()\n...     completed = State(final=True)\n...\n...     add_to_order = waiting_for_payment.to(waiting_for_payment)\n...     receive_payment = (\n...         waiting_for_payment.to(processing, cond=\"payments_enough\")\n...         | waiting_for_payment.to(waiting_for_payment, unless=\"payments_enough\")\n...     )\n...     process_order = processing.to(shipping, cond=\"payment_received\")\n...     ship_order = shipping.to(completed)\n...\n...     def __init__(self):\n...         self.order_total = 0\n...         self.payments = []\n...         self.payment_received = False\n...         super(OrderControl, self).__init__()\n...\n...     def payments_enough(self, amount):\n...         return sum(self.payments) + amount \u003e= self.order_total\n...\n...     def before_add_to_order(self, amount):\n...         self.order_total += amount\n...         return self.order_total\n...\n...     def before_receive_payment(self, amount):\n...         self.payments.append(amount)\n...         return self.payments\n...\n...     def after_receive_payment(self):\n...         self.payment_received = True\n...\n...     def on_enter_waiting_for_payment(self):\n...         self.payment_received = False\n\n```\n\nYou can use this machine as follows.\n\n```py\n\u003e\u003e\u003e control = OrderControl()\n\n\u003e\u003e\u003e control.add_to_order(3)\n3\n\n\u003e\u003e\u003e control.add_to_order(7)\n10\n\n\u003e\u003e\u003e control.receive_payment(4)\n[4]\n\n\u003e\u003e\u003e control.current_state.id\n'waiting_for_payment'\n\n\u003e\u003e\u003e control.current_state.name\n'Waiting for payment'\n\n\u003e\u003e\u003e control.process_order()\nTraceback (most recent call last):\n...\nstatemachine.exceptions.TransitionNotAllowed: Can't process_order when in Waiting for payment.\n\n\u003e\u003e\u003e control.receive_payment(6)\n[4, 6]\n\n\u003e\u003e\u003e control.current_state.id\n'processing'\n\n\u003e\u003e\u003e control.process_order()\n\n\u003e\u003e\u003e control.ship_order()\n\n\u003e\u003e\u003e control.payment_received\nTrue\n\n\u003e\u003e\u003e control.order_total\n10\n\n\u003e\u003e\u003e control.payments\n[4, 6]\n\n\u003e\u003e\u003e control.completed.is_active\nTrue\n\n```\n\nThere's a lot more to cover, please take a look at our docs:\nhttps://python-statemachine.readthedocs.io.\n\n\n## Contributing\n\n* \u003ca class=\"github-button\" href=\"https://github.com/fgmacedo/python-statemachine\" data-icon=\"octicon-star\" aria-label=\"Star fgmacedo/python-statemachine on GitHub\"\u003eStar this project\u003c/a\u003e\n* \u003ca class=\"github-button\" href=\"https://github.com/fgmacedo/python-statemachine/issues\" data-icon=\"octicon-issue-opened\" aria-label=\"Issue fgmacedo/python-statemachine on GitHub\"\u003eOpen an Issue\u003c/a\u003e\n* \u003ca class=\"github-button\" href=\"https://github.com/fgmacedo/python-statemachine/fork\" data-icon=\"octicon-repo-forked\" aria-label=\"Fork fgmacedo/python-statemachine on GitHub\"\u003eFork\u003c/a\u003e\n\n- If you found this project helpful, please consider giving it a star on GitHub.\n\n- **Contribute code**: If you would like to contribute code, please submit a pull\nrequest. For more information on how to contribute, please see our [contributing.md](contributing.md) file.\n\n- **Report bugs**: If you find any bugs, please report them by opening an issue\n  on our GitHub issue tracker.\n\n- **Suggest features**: If you have an idea for a new feature, of feels something being harder than it should be,\n  please let us know by opening an issue on our GitHub issue tracker.\n\n- **Documentation**: Help improve documentation by submitting pull requests.\n\n- **Promote the project**: Help spread the word by sharing on social media,\n  writing a blog post, or giving a talk about it. Tag me on Twitter\n  [@fgmacedo](https://twitter.com/fgmacedo) so I can share it too!\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffgmacedo%2Fpython-statemachine","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ffgmacedo%2Fpython-statemachine","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ffgmacedo%2Fpython-statemachine/lists"}