Skip to content

serialx/pygenfsm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

11 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ”„ pygenfsm

PyPI version Python License: MIT Code style: ruff Type checked: pyright Test Coverage

A minimal, clean, typed and async-native FSM (Finite State Machine) implementation for Python, inspired by Erlang's gen_fsm

Installation β€’ Quick Start β€’ Features β€’ Examples β€’ API Reference β€’ Contributing


🎯 Why pygenfsm?

Building robust state machines in Python often involves:

  • 🀯 Complex if/elif chains that grow unmaintainable
  • πŸ› Implicit state that's hard to reason about
  • πŸ”€ Scattered transition logic across your codebase
  • ❌ No type safety for states and events
  • 🚫 Mixing sync and async code awkwardly

pygenfsm solves these problems with a minimal, elegant API that leverages Python's type system and async capabilities.

✨ Features

🎨 Clean API

@fsm.on(State.IDLE, StartEvent)
def handle_start(fsm, event):
    return State.RUNNING

πŸ”„ Async Native

@fsm.on(State.RUNNING, DataEvent)
async def handle_data(fsm, event):
    await process_data(event.data)
    return State.DONE

🎯 Type Safe

# Full typing with generics
FSM[StateEnum, EventType, ContextType]

πŸš€ Zero Dependencies

# Minimal and fast
pip install pygenfsm

Key Benefits

  • πŸ”’ Type-safe: Full typing support with generics for states, events, and context
  • 🎭 Flexible: Mix sync and async handlers in the same FSM
  • πŸ“¦ Minimal: Zero dependencies, clean API surface
  • 🐍 Pythonic: Decorator-based, intuitive design
  • πŸ”„ Async-native: Built for modern async Python
  • πŸ“Š Context-aware: Carry data between transitions
  • 🧬 Cloneable: Fork FSM instances for testing scenarios
  • πŸ—οΈ Builder pattern: Late context injection support

πŸ“¦ Installation

# Using pip
pip install pygenfsm

# Using uv (recommended)
uv add pygenfsm

# Using poetry
poetry add pygenfsm

πŸš€ Quick Start

Basic Example

import asyncio
from dataclasses import dataclass
from enum import Enum, auto
from pygenfsm import FSM

# 1. Define states as an enum
class State(Enum):
    IDLE = auto()
    RUNNING = auto()
    DONE = auto()

# 2. Define events as dataclasses
@dataclass
class StartEvent:
    task_id: str

@dataclass
class CompleteEvent:
    result: str

# 3. Create FSM with initial state
fsm = FSM[State, StartEvent | CompleteEvent, None](
    state=State.IDLE,
    context=None,  # No context needed for simple FSM
)

# 4. Define handlers with decorators
@fsm.on(State.IDLE, StartEvent)
def start_handler(fsm, event: StartEvent) -> State:
    print(f"Starting task {event.task_id}")
    return State.RUNNING

@fsm.on(State.RUNNING, CompleteEvent)
def complete_handler(fsm, event: CompleteEvent) -> State:
    print(f"Task completed: {event.result}")
    return State.DONE

# 5. Run the FSM
async def main():
    await fsm.send(StartEvent(task_id="123"))
    await fsm.send(CompleteEvent(result="Success!"))
    print(f"Final state: {fsm.state}")

asyncio.run(main())

🎯 Core Concepts

States, Events, and Context

pygenfsm is built on three core concepts:

Concept Purpose Implementation
States The finite set of states your system can be in Python Enum
Events Things that happen to trigger transitions Dataclasses
Context Data that persists across transitions Any Python type

Handler Types

pygenfsm seamlessly supports both sync and async handlers:

# Sync handler - for simple state transitions
@fsm.on(State.IDLE, SimpleEvent)
def sync_handler(fsm, event) -> State:
    # Fast, synchronous logic
    return State.NEXT

# Async handler - for I/O operations
@fsm.on(State.LOADING, DataEvent)
async def async_handler(fsm, event) -> State:
    # Async I/O, network calls, etc.
    data = await fetch_data(event.url)
    fsm.context.data = data
    return State.READY

πŸ“š Examples

Traffic Light System

from enum import Enum, auto
from dataclasses import dataclass
from pygenfsm import FSM

class Color(Enum):
    RED = auto()
    YELLOW = auto()
    GREEN = auto()

@dataclass
class TimerEvent:
    """Timer expired event"""
    pass

@dataclass
class EmergencyEvent:
    """Emergency button pressed"""
    pass

# Create FSM
traffic_light = FSM[Color, TimerEvent | EmergencyEvent, None](
    state=Color.RED,
    context=None,
)

@traffic_light.on(Color.RED, TimerEvent)
def red_to_green(fsm, event) -> Color:
    print("πŸ”΄ β†’ 🟒")
    return Color.GREEN

@traffic_light.on(Color.GREEN, TimerEvent)
def green_to_yellow(fsm, event) -> Color:
    print("🟒 β†’ 🟑")
    return Color.YELLOW

@traffic_light.on(Color.YELLOW, TimerEvent)
def yellow_to_red(fsm, event) -> Color:
    print("🟑 β†’ πŸ”΄")
    return Color.RED

# Emergency overrides from any state
for color in Color:
    @traffic_light.on(color, EmergencyEvent)
    def emergency(fsm, event) -> Color:
        print("🚨 EMERGENCY β†’ RED")
        return Color.RED

Connection Manager with Retry Logic

import asyncio
from dataclasses import dataclass, field
from enum import Enum, auto
from pygenfsm import FSM

class ConnState(Enum):
    DISCONNECTED = auto()
    CONNECTING = auto()
    CONNECTED = auto()
    ERROR = auto()

@dataclass
class ConnectEvent:
    host: str
    port: int

@dataclass
class ConnectionContext:
    retries: int = 0
    max_retries: int = 3
    last_error: str = ""

fsm = FSM[ConnState, ConnectEvent, ConnectionContext](
    state=ConnState.DISCONNECTED,
    context=ConnectionContext(),
)

@fsm.on(ConnState.DISCONNECTED, ConnectEvent)
async def start_connection(fsm, event: ConnectEvent) -> ConnState:
    print(f"πŸ”Œ Connecting to {event.host}:{event.port}")
    return ConnState.CONNECTING

@fsm.on(ConnState.CONNECTING, ConnectEvent)
async def attempt_connect(fsm, event: ConnectEvent) -> ConnState:
    try:
        # Simulate connection attempt
        await asyncio.sleep(1)
        if fsm.context.retries < 2:  # Simulate failures
            raise ConnectionError("Network timeout")
        
        print("βœ… Connected!")
        fsm.context.retries = 0
        return ConnState.CONNECTED
        
    except ConnectionError as e:
        fsm.context.retries += 1
        fsm.context.last_error = str(e)
        
        if fsm.context.retries >= fsm.context.max_retries:
            print(f"❌ Max retries reached: {e}")
            return ConnState.ERROR
        
        print(f"πŸ”„ Retry {fsm.context.retries}/{fsm.context.max_retries}")
        return ConnState.CONNECTING

πŸ—οΈ Advanced Patterns

Late Context Injection with FSMBuilder

Perfect for dependency injection and testing:

from pygenfsm import FSMBuilder

# Define builder without context
builder = FSMBuilder[State, Event, AppContext](
    initial_state=State.INIT
)

@builder.on(State.INIT, StartEvent)
async def initialize(fsm, event) -> State:
    # Access context that will be injected later
    await fsm.context.database.connect()
    return State.READY

# Later, when dependencies are ready...
database = Database(connection_string)
logger = Logger(level="INFO")

# Build FSM with context
fsm = builder.build(AppContext(
    database=database,
    logger=logger,
))

Cloning for Testing Scenarios

Test different paths without affecting the original:

# Create base FSM
original_fsm = FSM[State, Event, Context](
    state=State.INITIAL,
    context=Context(data=[]),
)

# Clone for testing
test_scenario_1 = original_fsm.clone()
test_scenario_2 = original_fsm.clone()

# Run different scenarios
await test_scenario_1.send(SuccessEvent())
await test_scenario_2.send(FailureEvent())

# Original remains unchanged
assert original_fsm.state == State.INITIAL

πŸ”Œ API Reference

Core Classes

FSM[S, E, C]

The main FSM class with generic parameters:

  • S: State enum type
  • E: Event type (can be a Union)
  • C: Context type

Methods:

  • on(state: S, event_type: type[E]): Decorator to register handlers
  • async send(event: E) -> S: Send event and transition state
  • send_sync(event: E) -> S: Synchronous send (only for sync handlers)
  • clone() -> FSM[S, E, C]: Create independent copy
  • replace_context(context: C) -> None: Replace context

FSMBuilder[S, E, C]

Builder for late context injection:

  • on(state: S, event_type: type[E]): Register handlers
  • build(context: C) -> FSM[S, E, C]: Create FSM with context

Best Practices

  1. Use sync handlers for:

    • Simple state transitions
    • Pure computations
    • Context updates
  2. Use async handlers for:

    • Network I/O
    • Database operations
    • File system access
    • Long computations
  3. Event Design:

    • Make events immutable (use frozen dataclasses)
    • Include all necessary data in events
    • Use Union types for multiple events per state
  4. Context Design:

    • Keep context focused and minimal
    • Use dataclasses for structure
    • Avoid circular references

🀝 Contributing

We love contributions! Please see our Contributing Guide for details.

# Setup development environment
git clone https://github.com/serialx/pygenfsm
cd pygenfsm
uv sync

# Run tests
uv run pytest

# Run linting
uv run ruff check .
uv run pyright .

πŸ“Š Comparison with transitions

Feature Comparison

Feature pygenfsm transitions
Event Data βœ… First-class with dataclasses ❌ Limited (callbacks, conditions)
Async Support βœ… Native async/await ❌ No built-in support
Type Safety βœ… Full generics ⚠️ Runtime checks only
State Definition βœ… Enums (type-safe) ⚠️ Strings/objects
Handler Registration βœ… Decorators ❌ Configuration dicts
Context/Model βœ… Explicit, typed ⚠️ Implicit on model
Dependencies βœ… Zero ❌ Multiple (six, etc.)
Visualization ❌ Not built-in βœ… GraphViz support
Hierarchical States ❌ No βœ… Yes (HSM)
Parallel States ❌ No βœ… Yes
State History ❌ No βœ… Yes
Guards/Conditions ⚠️ In handler logic βœ… Built-in
Callbacks ⚠️ In handlers βœ… before/after/prepare
Size ~300 LOC ~3000 LOC

When to Use Each

Use pygenfsm when you need:

  • πŸ”’ Strong type safety with IDE support
  • πŸ”„ Native async/await support
  • πŸ“¦ Zero dependencies
  • 🎯 Event-driven architecture with rich data
  • πŸš€ Modern Python patterns (3.11+)
  • πŸ§ͺ Easy testing with full typing

Use transitions when you need:

  • πŸ“Š State diagram visualization
  • πŸŽ„ Hierarchical states (HSM)
  • ⚑ Parallel state machines
  • πŸ“œ State history tracking
  • πŸ”„ Complex transition guards/conditions
  • πŸ—οΈ Legacy Python support

πŸ”— Links

πŸ“œ License

MIT License - see LICENSE file for details.


Made with ❀️ by developers who love clean state machines

About

A minimal, clean, typed and asynchronous FSM implementation inspired by Erlang's gen_fsm

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 2

  •  
  •  

Languages