CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/pypi-python-statemachine

Python finite state machine library with declarative API for sync and async applications

Pending
Overview
Eval results
Files

exceptions.mddocs/

Exceptions and Error Handling

Error scenarios, exception types, and error handling patterns including transition validation errors, invalid state values, and configuration errors.

Capabilities

Exception Hierarchy

Comprehensive exception hierarchy for different types of state machine errors.

class StateMachineError(Exception):
    """
    Base exception for all state machine errors.
    
    All exceptions raised by the state machine library inherit from this class,
    making it easy to catch any state machine-related error.
    """

class InvalidDefinition(StateMachineError):
    """
    Raised when the state machine has a definition error.
    
    This includes errors in state machine class definition, invalid state
    configurations, or incorrect transition specifications.
    """

class InvalidStateValue(InvalidDefinition):
    """
    Raised when the current model state value is not mapped to a state definition.
    
    Occurs when external model has a state value that doesn't correspond
    to any defined State object in the state machine.
    
    Attributes:
    - value: The invalid state value that was encountered
    """
    def __init__(self, value, msg=None): ...
    
    @property
    def value(self):
        """Get the invalid state value."""

class AttrNotFound(InvalidDefinition):
    """
    Raised when there's no method or property with the given name.
    
    Occurs when callback references point to non-existent methods or
    when accessing undefined state machine attributes.
    """

class TransitionNotAllowed(StateMachineError):
    """
    Raised when there's no transition that can run from the current state.
    
    This is the most common runtime exception, occurring when trying to
    trigger an event that has no valid transition from the current state.
    
    Attributes:
    - event: The Event object that couldn't be processed
    - state: The State object where the transition was attempted
    """
    def __init__(self, event: Event, state: State): ...
    
    @property 
    def event(self) -> Event:
        """Get the event that couldn't be processed."""
    
    @property
    def state(self) -> State:
        """Get the state where transition was attempted."""

Usage Examples

Handling TransitionNotAllowed

from statemachine import StateMachine, State
from statemachine.exceptions import TransitionNotAllowed

class LightSwitch(StateMachine):
    off = State(initial=True)
    on = State()
    
    turn_on = off.to(on)
    turn_off = on.to(off)

# Basic exception handling
switch = LightSwitch()

try:
    switch.send("turn_off")  # Can't turn off when already off
except TransitionNotAllowed as e:
    print(f"Cannot {e.event.name} when in {e.state.name}")
    print(f"Current state: {switch.current_state.id}")
    # Output: Cannot turn_off when in Off
    #         Current state: off

# Checking allowed events before sending
if "turn_on" in [event.id for event in switch.allowed_events]:
    switch.send("turn_on")
else:
    print("Turn on event not allowed")

# Alternative: Check if event is allowed
if switch.is_allowed("turn_on"):
    switch.send("turn_on")

Graceful Error Handling with Configuration

class RobustMachine(StateMachine):
    state1 = State(initial=True)
    state2 = State()
    state3 = State(final=True)
    
    advance = state1.to(state2) | state2.to(state3)
    
    def __init__(self, *args, **kwargs):
        # Allow events without transitions (won't raise exceptions)
        super().__init__(*args, allow_event_without_transition=True, **kwargs)
    
    def handle_unknown_event(self, event: str):
        """Custom handler for events without transitions."""
        print(f"Event '{event}' ignored - no valid transition")

# Usage
machine = RobustMachine()
machine.send("unknown_event")  # Won't raise exception
machine.send("advance")        # Normal transition
machine.send("unknown_event")  # Still won't raise exception

Handling InvalidStateValue with External Models

from statemachine.exceptions import InvalidStateValue

class Document:
    def __init__(self):
        self.status = "invalid_status"  # This will cause an error

class DocumentMachine(StateMachine):
    draft = State(initial=True, value="draft_status")
    review = State(value="review_status")
    published = State(value="published_status", final=True)
    
    submit = draft.to(review)
    publish = review.to(published)

# Handle invalid external state
doc = Document()

try:
    machine = DocumentMachine(doc, state_field="status")
except InvalidStateValue as e:
    print(f"Invalid state value: {e.value}")
    print("Available states:", [s.value for s in DocumentMachine._states()])
    
    # Fix the model and retry
    doc.status = "draft_status"
    machine = DocumentMachine(doc, state_field="status")
    print(f"Current state: {machine.current_state.id}")

Custom Error Handling in Callbacks

class ValidatedMachine(StateMachine):
    idle = State(initial=True)
    processing = State()
    completed = State(final=True)
    error = State(final=True)
    
    start = idle.to(processing)
    complete = processing.to(completed)
    fail = processing.to(error)
    
    def before_start(self, data: dict = None):
        """Validator that may raise exceptions."""
        if not data:
            raise ValueError("Data is required to start processing")
        
        if not isinstance(data, dict):
            raise TypeError("Data must be a dictionary")
        
        required_fields = ["id", "type", "content"]
        missing = [field for field in required_fields if field not in data]
        if missing:
            raise ValueError(f"Missing required fields: {missing}")
        
        print("Validation passed")
    
    def on_start(self, data: dict = None):
        """Action that may fail."""
        try:
            self.process_data(data)
        except Exception as e:
            print(f"Processing failed: {e}")
            # Trigger failure transition
            self.send("fail")
            raise  # Re-raise to caller
    
    def process_data(self, data: dict):
        """Simulate data processing that may fail."""
        if data.get("type") == "error_test":
            raise RuntimeError("Simulated processing error")
        print(f"Successfully processed data: {data['id']}")

# Usage with error handling
machine = ValidatedMachine()

# Test validation errors
try:
    machine.send("start")  # Missing data
except ValueError as e:
    print(f"Validation error: {e}")

try:
    machine.send("start", data="not_a_dict")  # Wrong type
except TypeError as e:
    print(f"Type error: {e}")

try:
    machine.send("start", data={"id": "123"})  # Missing fields
except ValueError as e:
    print(f"Validation error: {e}")

# Test processing error
try:
    machine.send("start", data={"id": "test", "type": "error_test", "content": "test"})
except RuntimeError as e:
    print(f"Processing error: {e}")
    print(f"Machine state after error: {machine.current_state.id}")

# Successful processing
try:
    machine = ValidatedMachine()  # Reset machine
    machine.send("start", data={"id": "test", "type": "normal", "content": "test"})
    machine.send("complete")
    print(f"Final state: {machine.current_state.id}")
except Exception as e:
    print(f"Unexpected error: {e}")

Error Recovery Patterns

class RecoveryMachine(StateMachine):
    normal = State(initial=True)
    error = State()
    recovery = State()
    failed = State(final=True)
    
    trigger_error = normal.to(error)
    attempt_recovery = error.to(recovery)
    recover = recovery.to(normal)
    give_up = error.to(failed) | recovery.to(failed)
    
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.error_count = 0
        self.max_retries = 3
    
    def on_enter_error(self, error_info: str = "Unknown error"):
        """Handle entering error state."""
        self.error_count += 1
        print(f"Error #{self.error_count}: {error_info}")
        
        if self.error_count <= self.max_retries:
            print("Attempting recovery...")
            self.send("attempt_recovery")
        else:
            print("Max retries exceeded, giving up")
            self.send("give_up")
    
    def on_enter_recovery(self):
        """Attempt recovery."""
        import random
        if random.choice([True, False]):  # 50% success rate
            print("Recovery successful!")
            self.error_count = 0  # Reset error count
            self.send("recover")
        else:
            print("Recovery failed")
            self.send("give_up")
    
    def on_enter_failed(self):
        """Handle permanent failure."""
        print("System has failed permanently")

# Usage
machine = RecoveryMachine()

# Simulate multiple errors
for i in range(5):
    try:
        if machine.current_state.id == "normal":
            machine.send("trigger_error", error_info=f"Error scenario {i+1}")
        # Machine handles recovery automatically
        if machine.current_state.id in ["failed"]:
            break
    except Exception as e:
        print(f"Unexpected error: {e}")
        break

print(f"Final state: {machine.current_state.id}")

Debugging and Error Logging

import logging
from statemachine.exceptions import StateMachineError

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

class LoggingMachine(StateMachine):
    start = State(initial=True)
    middle = State()
    end = State(final=True)
    error = State(final=True)
    
    proceed = start.to(middle) | middle.to(end)
    fail = start.to(error) | middle.to(error)
    
    def on_transition(self, event: str, source: State, target: State, **kwargs):
        """Log all transitions."""
        logger.info(f"Transition: {event} ({source.id} -> {target.id})")
    
    def on_enter_state(self, state: State, **kwargs):
        """Log state entries."""
        logger.info(f"Entering state: {state.name}")
    
    def send(self, event: str, *args, **kwargs):
        """Override send to add error logging."""
        try:
            logger.debug(f"Sending event: {event} with args={args}, kwargs={kwargs}")
            result = super().send(event, *args, **kwargs)
            logger.debug(f"Event {event} processed successfully")
            return result
        except StateMachineError as e:
            logger.error(f"State machine error in event {event}: {e}")
            logger.error(f"Current state: {self.current_state.id}")
            logger.error(f"Allowed events: {[ev.id for ev in self.allowed_events]}")
            raise
        except Exception as e:
            logger.error(f"Unexpected error in event {event}: {e}")
            # Try to transition to error state if possible
            if self.is_allowed("fail"):
                logger.info("Attempting automatic error state transition")
                super().send("fail")
            raise

# Usage with logging
machine = LoggingMachine()
machine.send("proceed")
try:
    machine.send("invalid_event")
except TransitionNotAllowed:
    logger.warning("Handled transition not allowed error")

machine.send("fail")  # Transition to error state

Install with Tessl CLI

npx tessl i tessl/pypi-python-statemachine

docs

actions-callbacks.md

core-statemachine.md

diagrams.md

events-transitions.md

exceptions.md

index.md

mixins-integration.md

utilities.md

tile.json