State

The State Pattern is a behavioral design pattern that allows an object to alter its behavior when its internal state changes. The pattern encapsulates state-specific behavior into separate state classes and delegates behavior to the current state object, making it appear as if the object changed its class.

Core Concept

Instead of using complex conditional statements to handle different behaviors based on an object’s state, the State Pattern extracts each state into a separate class that implements a common interface. The context object maintains a reference to one of these state objects and delegates all state-dependent operations to it.

Structure

┌─────────────┐    uses    ┌─────────────┐
│   Context   │◆──────────▶│    State    │
├─────────────┤            ├─────────────┤
│- state      │            │+ handle()   │
│+ request()  │            └─────────────┘
│+ setState() │                    △
└─────────────┘                    │
                                   │
                    ┌──────────────┼──────────────┐
                    │              │              │
            ┌───────▼──────┐ ┌───────▼──────┐ ┌───────▼──────┐
            │ConcreteStateA│ │ConcreteStateB│ │ConcreteStateC│
            ├──────────────┤ ├──────────────┤ ├──────────────┤
            │+ handle()    │ │+ handle()    │ │+ handle()    │
            └──────────────┘ └──────────────┘ └──────────────┘

Key Components

State Interface/Abstract Class: Defines the interface for encapsulating behavior associated with a particular state.

Concrete States: Implement behavior associated with a specific state of the context.

Context: Maintains a reference to a state object and delegates state-specific requests to this object.

Benefits

  • Eliminates complex conditionals: Removes lengthy if-else or switch statements
  • Localizes state-specific behavior: Each state’s behavior is contained in its own class
  • Makes state transitions explicit: State changes are clearly defined operations
  • Supports Open/Closed Principle: New states can be added without modifying existing code
  • Improves maintainability: Changes to one state don’t affect others

When to Use

  • An object’s behavior depends on its state and must change at runtime
  • Operations have large, multipart conditional statements that depend on object state
  • State-specific behavior is scattered across multiple methods
  • You need to avoid duplicate code when similar states share common behavior

Implementation Considerations

State Transitions: States can trigger their own transitions or leave transition control to the context.

State Storage: The context typically stores the current state, but states can also be stored as singletons if they’re stateless.

State Creation: States can be created on-demand or pre-instantiated depending on performance and memory requirements.

Example 1: Media Player

This example demonstrates a media player that behaves differently based on its current state. The MediaPlayer context delegates all operations to the current state object. Each state (PlayingState, PausedState, StoppedState) implements different behaviors for the same operations.

from abc import ABC, abstractmethod
from typing import TYPE_CHECKING

if TYPE_CHECKING:
    from __future__ import annotations

class MediaPlayerState(ABC):
    """Abstract base class for media player states."""
    
    @abstractmethod
    def play(self, player: 'MediaPlayer') -> None:
        """Handle play action for this state."""
        pass
    
    @abstractmethod
    def pause(self, player: 'MediaPlayer') -> None:
        """Handle pause action for this state."""
        pass
    
    @abstractmethod
    def stop(self, player: 'MediaPlayer') -> None:
        """Handle stop action for this state."""
        pass
    
    @abstractmethod
    def get_status(self) -> str:
        """Return current state status."""
        pass


class PlayingState(MediaPlayerState):
    """Concrete state representing the playing state."""
    
    def play(self, player: 'MediaPlayer') -> None:
        print("Already playing. Media continues...")
    
    def pause(self, player: 'MediaPlayer') -> None:
        print("Pausing playback...")
        player.set_state(PausedState())
    
    def stop(self, player: 'MediaPlayer') -> None:
        print("Stopping playback...")
        player.set_state(StoppedState())
    
    def get_status(self) -> str:
        return "Playing"


class PausedState(MediaPlayerState):
    """Concrete state representing the paused state."""
    
    def play(self, player: 'MediaPlayer') -> None:
        print("Resuming playback...")
        player.set_state(PlayingState())
    
    def pause(self, player: 'MediaPlayer') -> None:
        print("Already paused.")
    
    def stop(self, player: 'MediaPlayer') -> None:
        print("Stopping from pause...")
        player.set_state(StoppedState())
    
    def get_status(self) -> str:
        return "Paused"


class StoppedState(MediaPlayerState):
    """Concrete state representing the stopped state."""
    
    def play(self, player: 'MediaPlayer') -> None:
        print("Starting playback...")
        player.set_state(PlayingState())
    
    def pause(self, player: 'MediaPlayer') -> None:
        print("Cannot pause. Media is stopped. Press play first.")
    
    def stop(self, player: 'MediaPlayer') -> None:
        print("Already stopped.")
    
    def get_status(self) -> str:
        return "Stopped"


class MediaPlayer:
    """Context class that uses state objects to handle behavior."""
    
    def __init__(self) -> None:
        # Initialize with stopped state
        self._state: MediaPlayerState = StoppedState()
        self._current_track: str = "No track loaded"
    
    def set_state(self, state: MediaPlayerState) -> None:
        """Change the current state."""
        self._state = state
    
    def play(self) -> None:
        """Delegate play action to current state."""
        self._state.play(self)
    
    def pause(self) -> None:
        """Delegate pause action to current state."""
        self._state.pause(self)
    
    def stop(self) -> None:
        """Delegate stop action to current state."""
        self._state.stop(self)
    
    def load_track(self, track_name: str) -> None:
        """Load a new track (available in any state)."""
        self._current_track = track_name
        print(f"Loaded track: {track_name}")
    
    def get_status(self) -> str:
        """Get current player status."""
        return f"Status: {self._state.get_status()} | Track: {self._current_track}"


# Demonstration
if __name__ == "__main__":
    player = MediaPlayer()
    
    print("=== Media Player State Pattern Demo ===")
    print(player.get_status())
    print()
    
    # Try to pause when stopped
    player.pause()
    print()
    
    # Load and play a track
    player.load_track("Bohemian Rhapsody")
    player.play()
    print(player.get_status())
    print()
    
    # Pause the track
    player.pause()
    print(player.get_status())
    print()
    
    # Resume playing
    player.play()
    print(player.get_status())
    print()
    
    # Stop the player
    player.stop()
    print(player.get_status())
    print()
    
    # Try to play when stopped
    player.play()
    print(player.get_status())

Key Points:

  • State transitions are handled within the state classes themselves
  • Each state knows which transitions are valid from its current position
  • The context remains simple and focused on delegation
  • Adding new states (like “Loading” or “Buffering”) requires no changes to existing code

Example 2: Document Workflow

This example shows a document workflow system where documents progress through different states: Draft → Under Review → Approved → Published. Each state enforces different rules about what operations are allowed.

from abc import ABC, abstractmethod
from datetime import datetime
from typing import TYPE_CHECKING, List, Optional

if TYPE_CHECKING:
    from __future__ import annotations

class DocumentState(ABC):
    """Abstract base class for document workflow states."""
    
    @abstractmethod
    def edit(self, document: 'Document', content: str) -> bool:
        """Attempt to edit the document."""
        pass
    
    @abstractmethod
    def submit_for_review(self, document: 'Document') -> bool:
        """Attempt to submit document for review."""
        pass
    
    @abstractmethod
    def approve(self, document: 'Document', reviewer: str) -> bool:
        """Attempt to approve the document."""
        pass
    
    @abstractmethod
    def reject(self, document: 'Document', reason: str) -> bool:
        """Attempt to reject the document."""
        pass
    
    @abstractmethod
    def publish(self, document: 'Document') -> bool:
        """Attempt to publish the document."""
        pass
    
    @abstractmethod
    def get_state_name(self) -> str:
        """Return the name of this state."""
        pass


class DraftState(DocumentState):
    """Document is in draft mode - can be edited freely."""
    
    def edit(self, document: 'Document', content: str) -> bool:
        document._content = content
        document._last_modified = datetime.now()
        document._add_log("Document edited in draft state")
        return True
    
    def submit_for_review(self, document: 'Document') -> bool:
        if not document._content.strip():
            document._add_log("Cannot submit empty document for review")
            return False
        document._add_log("Document submitted for review")
        document._set_state(UnderReviewState())
        return True
    
    def approve(self, document: 'Document', reviewer: str) -> bool:
        document._add_log("Cannot approve document in draft state")
        return False
    
    def reject(self, document: 'Document', reason: str) -> bool:
        document._add_log("Cannot reject document in draft state")
        return False
    
    def publish(self, document: 'Document') -> bool:
        document._add_log("Cannot publish document directly from draft")
        return False
    
    def get_state_name(self) -> str:
        return "Draft"


class UnderReviewState(DocumentState):
    """Document is under review - editing restricted."""
    
    def edit(self, document: 'Document', content: str) -> bool:
        document._add_log("Cannot edit document while under review")
        return False
    
    def submit_for_review(self, document: 'Document') -> bool:
        document._add_log("Document is already under review")
        return False
    
    def approve(self, document: 'Document', reviewer: str) -> bool:
        document._add_log(f"Document approved by {reviewer}")
        document._approved_by = reviewer
        document._approved_date = datetime.now()
        document._set_state(ApprovedState())
        return True
    
    def reject(self, document: 'Document', reason: str) -> bool:
        document._add_log(f"Document rejected: {reason}")
        document._rejection_reason = reason
        document._set_state(DraftState())  # Back to draft for editing
        return True
    
    def publish(self, document: 'Document') -> bool:
        document._add_log("Cannot publish document that hasn't been approved")
        return False
    
    def get_state_name(self) -> str:
        return "Under Review"


class ApprovedState(DocumentState):
    """Document has been approved - ready for publishing."""
    
    def edit(self, document: 'Document', content: str) -> bool:
        document._add_log("Cannot edit approved document without returning to draft")
        return False
    
    def submit_for_review(self, document: 'Document') -> bool:
        document._add_log("Document is already approved")
        return False
    
    def approve(self, document: 'Document', reviewer: str) -> bool:
        document._add_log("Document is already approved")
        return False
    
    def reject(self, document: 'Document', reason: str) -> bool:
        document._add_log(f"Approved document rejected: {reason}")
        document._rejection_reason = reason
        document._set_state(DraftState())
        return True
    
    def publish(self, document: 'Document') -> bool:
        document._add_log("Document published")
        document._published_date = datetime.now()
        document._set_state(PublishedState())
        return True
    
    def get_state_name(self) -> str:
        return "Approved"


class PublishedState(DocumentState):
    """Document has been published - read-only."""
    
    def edit(self, document: 'Document', content: str) -> bool:
        document._add_log("Cannot edit published document")
        return False
    
    def submit_for_review(self, document: 'Document') -> bool:
        document._add_log("Published document cannot be submitted for review")
        return False
    
    def approve(self, document: 'Document', reviewer: str) -> bool:
        document._add_log("Published document cannot be approved")
        return False
    
    def reject(self, document: 'Document', reason: str) -> bool:
        document._add_log("Published document cannot be rejected")
        return False
    
    def publish(self, document: 'Document') -> bool:
        document._add_log("Document is already published")
        return False
    
    def get_state_name(self) -> str:
        return "Published"


class Document:
    """Context class representing a document in a workflow system."""
    
    def __init__(self, title: str, author: str) -> None:
        self._title: str = title
        self._author: str = author
        self._content: str = ""
        self._state: DocumentState = DraftState()
        self._created_date: datetime = datetime.now()
        self._last_modified: datetime = datetime.now()
        self._log: List[str] = []
        self._approved_by: Optional[str] = None
        self._approved_date: Optional[datetime] = None
        self._published_date: Optional[datetime] = None
        self._rejection_reason: Optional[str] = None
        
        self._add_log(f"Document '{title}' created by {author}")
    
    def _set_state(self, state: DocumentState) -> None:
        """Change the document's state."""
        self._state = state
    
    def _add_log(self, message: str) -> None:
        """Add an entry to the document's activity log."""
        timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        self._log.append(f"[{timestamp}] {message}")
    
    # Public interface methods that delegate to the current state
    
    def edit(self, content: str) -> bool:
        """Edit the document content."""
        return self._state.edit(self, content)
    
    def submit_for_review(self) -> bool:
        """Submit the document for review."""
        return self._state.submit_for_review(self)
    
    def approve(self, reviewer: str) -> bool:
        """Approve the document."""
        return self._state.approve(self, reviewer)
    
    def reject(self, reason: str) -> bool:
        """Reject the document."""
        return self._state.reject(self, reason)
    
    def publish(self) -> bool:
        """Publish the document."""
        return self._state.publish(self)
    
    def get_status(self) -> str:
        """Get current document status."""
        return (f"Title: {self._title} | Author: {self._author} | "
                f"State: {self._state.get_state_name()} | "
                f"Content Length: {len(self._content)} chars")
    
    def get_activity_log(self) -> List[str]:
        """Get the document's activity log."""
        return self._log.copy()


# Demonstration
if __name__ == "__main__":
    print("=== Document Workflow State Pattern Demo ===")
    
    # Create a new document
    doc = Document("API Documentation", "Alice Smith")
    print(f"Document created: {doc.get_status()}")
    print()
    
    # Edit the document (allowed in draft state)
    doc.edit("This is the initial content of the API documentation...")
    print("After editing:", doc.get_status())
    print()
    
    # Try to publish directly (should fail)
    print("Attempting to publish directly:")
    doc.publish()
    print()
    
    # Submit for review
    print("Submitting for review:")
    doc.submit_for_review()
    print(doc.get_status())
    print()
    
    # Try to edit while under review (should fail)
    print("Attempting to edit while under review:")
    doc.edit("Trying to modify content...")
    print()
    
    # Approve the document
    print("Approving document:")
    doc.approve("Bob Johnson")
    print(doc.get_status())
    print()
    
    # Publish the approved document
    print("Publishing document:")
    doc.publish()
    print(doc.get_status())
    print()
    
    # Try to edit published document (should fail)
    print("Attempting to edit published document:")
    doc.edit("Cannot change this!")
    print()
    
    # Show activity log
    print("=== Document Activity Log ===")
    for log_entry in doc.get_activity_log():
        print(log_entry)

Key Points:

  • Each state encapsulates the business rules for that particular workflow stage
  • State transitions follow a defined workflow path
  • The document context maintains all data while states control behavior
  • Invalid operations are gracefully handled with appropriate feedback
  • The system maintains an audit log of all state changes and operations

The State Pattern provides a clean, maintainable solution for complex state-dependent behavior while keeping the code organized and extensible.

Track your progress

Mark this subtopic as completed when you finish reading.