Skip to main content

philiprehberger-state-machine

Tests PyPI version Last updated

philiprehberger-state-machine

Lightweight finite state machine with guards, callbacks, and visualization.

Installation

pip install philiprehberger-state-machine

Usage

from philiprehberger_state_machine import StateMachine

sm = StateMachine(
    states=["pending", "confirmed", "shipped", "delivered"],
    initial="pending",
    transitions=[
        ("pending", "confirmed", "confirm"),
        ("confirmed", "shipped", "ship"),
        ("shipped", "delivered", "deliver"),
    ],
)

sm.trigger("confirm")
print(sm.state)  # "confirmed"

Checking Available Transitions

from philiprehberger_state_machine import StateMachine

sm = StateMachine(
    states=["pending", "confirmed", "shipped"],
    initial="pending",
    transitions=[
        ("pending", "confirmed", "confirm"),
        ("confirmed", "shipped", "ship"),
    ],
)

sm.can("confirm")  # True
sm.can("ship")     # False

# All events that can fire from the current state (deduped)
sm.available_events()  # ["confirm"]

# (event, to_state) pairs, for the current state or any other state
sm.transitions_from()             # [("confirm", "confirmed")]
sm.transitions_from("confirmed")  # [("ship", "shipped")]

transitions_from() follows the same resolution as trigger(): exact-match transitions first, then wildcards for events not already handled. Guards are not evaluated.

Final States

Mark terminal states with final_states. A final state may not have outgoing transitions, and wildcard transitions and timeouts never fire from it.

from philiprehberger_state_machine import InvalidTransitionError, StateMachine

sm = StateMachine(
    states=["pending", "delivered", "cancelled", "error"],
    initial="pending",
    transitions=[
        ("pending", "delivered", "deliver"),
        ("pending", "cancelled", "cancel"),
        ("*", "error", "fail"),
    ],
    final_states=["delivered", "cancelled"],
)

sm.trigger("cancel")
print(sm.is_final)  # True

try:
    sm.trigger("fail")  # wildcards do not apply to final states
except InvalidTransitionError:
    print("already finished")

Unknown final states, or transitions and timeouts defined from a final state, raise ValueError. Final states are included in snapshot(), checked by restore(), filled gray in to_dot(), and drawn as state --> [*] in to_mermaid().

Callbacks

from philiprehberger_state_machine import StateMachine

sm = StateMachine(
    states=["pending", "confirmed", "shipped"],
    initial="pending",
    transitions=[
        ("pending", "confirmed", "confirm"),
        ("confirmed", "shipped", "ship"),
    ],
)

sm.on_enter("confirmed", lambda state, event: print(f"Entered {state} via {event}"))
sm.on_exit("pending", lambda state, event: print(f"Left {state} via {event}"))

sm.trigger("confirm")
# Left pending via confirm
# Entered confirmed via confirm

Guard Conditions

Guards are optional callables that receive a context dict and must return True to allow the transition. If a guard returns falsy, InvalidTransitionError is raised.

from philiprehberger_state_machine import StateMachine

sm = StateMachine(
    states=["draft", "published"],
    initial="draft",
    transitions=[],
)

sm.add_transition("draft", "published", "publish", guard=lambda ctx: ctx.get("has_title", False))

sm.trigger("publish", context={"has_title": True})   # succeeds
print(sm.state)  # "published"

Transition Context

Pass a context dict to trigger() to share data with guards and callbacks.

sm.trigger("confirm", context={"user": "alice", "approved": True})

If no context is provided, an empty dict is passed to guards.

Wildcard Transitions

Use "*" as the source state to define a transition that can fire from any state.

from philiprehberger_state_machine import StateMachine

sm = StateMachine(
    states=["idle", "running", "paused", "error"],
    initial="idle",
    transitions=[
        ("idle", "running", "start"),
        ("running", "paused", "pause"),
        ("*", "error", "fail"),
    ],
)

sm.trigger("start")
print(sm.state)  # "running"

sm.trigger("fail")
print(sm.state)  # "error"

Wildcard transitions are checked after exact state matches, so a specific transition always takes priority.

Global transition hook

Register a listener that fires after every successful transition. Useful for logging, metrics, or auditing.

import logging
from philiprehberger_state_machine import StateMachine

log = logging.getLogger(__name__)

sm = StateMachine(
    states=["pending", "confirmed", "shipped"],
    initial="pending",
    transitions=[
        ("pending", "confirmed", "confirm"),
        ("confirmed", "shipped", "ship"),
    ],
)

unsubscribe = sm.on_transition(
    lambda fr, to, ev, ctx: log.info("transition %s -> %s via %s", fr, to, ev)
)

sm.trigger("confirm")
sm.trigger("ship")

unsubscribe()  # remove the listener

The callback receives (from_state, to_state, event, context). Listeners fire in registration order, after on_exit and on_enter callbacks. Listeners do not fire when a guard rejects a transition.

Transition History

Track all past transitions with timestamps using transition_history.

from philiprehberger_state_machine import StateMachine

sm = StateMachine(
    states=["pending", "confirmed", "shipped"],
    initial="pending",
    transitions=[
        ("pending", "confirmed", "confirm"),
        ("confirmed", "shipped", "ship"),
    ],
)

sm.trigger("confirm")
sm.trigger("ship")

for record in sm.transition_history:
    print(f"{record.from_state} -> {record.to_state} via {record.event} at {record.timestamp}")
# pending -> confirmed via confirm at 1711929600.123
# confirmed -> shipped via ship at 1711929600.456

History and Reset

from philiprehberger_state_machine import StateMachine

sm = StateMachine(
    states=["pending", "confirmed", "shipped"],
    initial="pending",
    transitions=[
        ("pending", "confirmed", "confirm"),
        ("confirmed", "shipped", "ship"),
    ],
)

sm.trigger("confirm")
sm.trigger("ship")
print(sm.history)  # ["pending", "confirmed"]

sm.reset()
print(sm.state)    # "pending"
print(sm.history)  # []

Timeout-Based Automatic Transitions

Define transitions that fire automatically after a state has been active for a given number of seconds.

from philiprehberger_state_machine import StateMachine
import time

sm = StateMachine(
    states=["idle", "processing", "timeout_state"],
    initial="idle",
    transitions=[("idle", "processing", "start")],
)

sm.add_timeout("processing", "timeout_state", seconds=5.0)

sm.trigger("start")
print(sm.state)  # "processing"

time.sleep(6)
print(sm.state)  # "timeout_state"

Snapshot and Restore

Capture and restore the machine's state and history for serialization or checkpointing.

from philiprehberger_state_machine import StateMachine

sm = StateMachine(
    states=["a", "b", "c"],
    initial="a",
    transitions=[("a", "b", "go"), ("b", "c", "go")],
)

sm.trigger("go")
snap = sm.snapshot()
print(snap)  # {"state": "b", "history": ["a"]}

sm.trigger("go")
print(sm.state)  # "c"

sm.restore(snap)
print(sm.state)  # "b"

Visualization Export

Export the state machine as a DOT (Graphviz) or Mermaid diagram string.

from philiprehberger_state_machine import StateMachine

sm = StateMachine(
    states=["pending", "confirmed", "shipped"],
    initial="pending",
    transitions=[
        ("pending", "confirmed", "confirm"),
        ("confirmed", "shipped", "ship"),
    ],
)

print(sm.to_dot())
# digraph StateMachine {
#     rankdir=LR;
#
#     "pending" [shape=doublecircle];
#     "confirmed" [shape=circle];
#     "shipped" [shape=circle];
#
#     "pending" -> "confirmed" [label="confirm"];
#     "confirmed" -> "shipped" [label="ship"];
# }

print(sm.to_mermaid())
# stateDiagram-v2
#     [*] --> pending
#     pending --> confirmed : confirm
#     confirmed --> shipped : ship

API

Function / Class Description
StateMachine(states, initial, transitions, final_states=None) Create a state machine with given states, initial state, transitions, and optional terminal states
StateMachine.state Current state (read-only property)
StateMachine.is_final Whether the current state is a final state (read-only property)
StateMachine.final_states Configured final states in declaration order (read-only property)
StateMachine.history List of past states (read-only property)
StateMachine.transition_history List of TransitionRecord objects with timestamps (read-only property)
StateMachine.trigger(event, context=None) Execute a transition or raise InvalidTransitionError (also raised from a final state). Pass optional context dict to guards.
StateMachine.can(event) Return whether the event is valid from the current state
StateMachine.available_events() List of events (deduped) that can fire from the current state, including wildcards
StateMachine.transitions_from(state=None) List of (event, to_state) pairs that can fire from a state (defaults to current), including wildcards
StateMachine.add_transition(from_state, to_state, event, guard=None) Add a transition with an optional guard callable. Use "*" as from_state for wildcard.
StateMachine.on_enter(state, callback) Register a callback for entering a state
StateMachine.on_exit(state, callback) Register a callback for exiting a state
StateMachine.on_transition(callback) Register a global listener fired after every successful transition with (from_state, to_state, event, context). Returns an unsubscribe closure.
StateMachine.remove_transition_listener(callback) Remove a previously registered transition listener. Returns True if removed.
StateMachine.reset() Reset to initial state and clear history
StateMachine.add_timeout(state, target, seconds) Define an automatic transition after seconds in state
StateMachine.snapshot() Return a serializable dict of current state and history (plus final_states when configured)
StateMachine.restore(snapshot) Restore the machine from a snapshot dict
StateMachine.to_dot() Return a DOT/Graphviz string of the state machine
StateMachine.to_mermaid() Return a Mermaid state diagram string
TransitionRecord Frozen dataclass with from_state, to_state, event, and timestamp fields
InvalidTransitionError Raised on invalid transitions; has .state and .event attributes

Development

pip install -e .
python -m pytest tests/ -v

Support

If you find this project useful:

⭐ Star the repo

🐛 Report issues

💡 Suggest features

❤️ Sponsor development

🌐 All Open Source Projects

💻 GitHub Profile

🔗 LinkedIn Profile

License

MIT

Metadata

Release files for philiprehberger-state-machine 0.7.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for philiprehberger-state-machine 0.7.0
File Size Uploaded Provenance Actions
philiprehberger_state_machine-0.7.0.tar.gz 197.7 kB Without provenance Details for philiprehberger_state_machine-0.7.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for philiprehberger-state-machine 0.7.0
File Interpreter ABI Platform Actions
philiprehberger_state_machine-0.7.0-py3-none-any.whl Python 3 none any Details for philiprehberger_state_machine-0.7.0-py3-none-any.whl

Total release size: 208.6 kB

Release files / philiprehberger_state_machine-0.7.0.tar.gz

Download URL philiprehberger_state_machine-0.7.0.tar.gz
Size 197.7 kB
Tags Source
SHA-256 checksum
How to use checksums
eeec3c37a2ad43d0ff9b22afe5ca3574d0ba979aad4bfa87ed33c1c089cf3a06
BLAKE2b-256 checksum
How to use checksums
6bdeb8a89eb88e6e9cc6356685c92477470cbbfe7360e7e2cd6a2b004e8f84e2
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Release files / philiprehberger_state_machine-0.7.0-py3-none-any.whl

Download URL philiprehberger_state_machine-0.7.0-py3-none-any.whl
Size 10.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
126fb173b9a4d7ef25fbdd444a9993984268026b89b38459f73ab47b087177ef
BLAKE2b-256 checksum
How to use checksums
d77510bc417245abc0c6349504543c03f9c5b4e9ecc12e6657c9093b9c11b9d8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.12.15

Security for philiprehberger-state-machine 0.7.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page