Skip to main content

🔄 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

📜 License

MIT License - see LICENSE file for details.


Made with ❤️ by developers who love clean state machines

Metadata

Release files for pygenfsm 1.0.1

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

Source distribution (sdist)

Source distribution for pygenfsm 1.0.1
File Size Uploaded
pygenfsm-1.0.1.tar.gz 41.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pygenfsm 1.0.1
File Interpreter ABI Platform
pygenfsm-1.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 50.7 kB

Release files / pygenfsm-1.0.1.tar.gz

Download URL pygenfsm-1.0.1.tar.gz
Size 41.8 kB
Tags Source
SHA-256 checksum
How to use checksums
cee9facd628fcb382f9c8af847d1c15ac9a1164ee293ec1ca7532bd8d1133e93
BLAKE2b-256 checksum
How to use checksums
4f749b753640b212553d35d1b8e553903243edd794ba9b31403226f282b20293
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.12.9

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 14, 2025.

Transparency log

Release files / pygenfsm-1.0.1-py3-none-any.whl

Download URL pygenfsm-1.0.1-py3-none-any.whl
Size 8.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
75703efc82bffb60e386ac44901d782c5ce49edcb789bdd2922fcc8831ae617f
BLAKE2b-256 checksum
How to use checksums
50bdeca879a1a8e4cddbef3118a307297ff65097bbafddf6220b89c24be4fca6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.12.9

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 14, 2025.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.1 This release

2 release files

1.0.0

2 release files

0.1.0

2 release files

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