8.5. State
EN: State
PL: Stan
Type: object
The State design pattern is a behavioral design pattern that allows an object to change its behavior when its internal state changes. This pattern is close to the concept of finite-state machines. The main idea is to declare each possible state of an object as a separate class and extract all state-specific behaviors into these classes.
In Python, we can implement the State pattern using classes and polymorphism. Here's a simple example:
First, we define an abstract base class State that represents the general interface for all states:
>>> from abc import ABC, abstractmethod
>>>
>>> class State(ABC):
... @abstractmethod
... def handle(self, context):
... pass
Then, we define concrete state classes that implement the handle
method
differently:
>>> class ConcreteStateA(State):
... def handle(self, context):
... print("Handling in State A")
... context.state = ConcreteStateB()
>>>
>>> class ConcreteStateB(State):
... def handle(self, context):
... print("Handling in State B")
... context.state = ConcreteStateA()
Next, we define a Context
class that maintains an instance of a State
subclass and delegates the request
to the current state:
>>> class Context:
... def __init__(self, state: State):
... self._state = state
...
... @property
... def state(self):
... return self._state
...
... @state.setter
... def state(self, state: State):
... self._state = state
...
... def request(self):
... self._state.handle(self)
Finally, we can use the Context
and State
classes like this:
>>> context = Context(ConcreteStateA())
>>> context.request()
Handling in State A
>>> context.request()
Handling in State B
>>> context.request()
Handling in State A
In this example, ConcreteStateA` and ``ConcreteStateB
are interchangeable
states that the Context` class can use. The ``Context
class doesn't need to
know the details of how the states perform their operations. It just calls
the handle
method on its current state object.
The State design pattern has several advantages:
Flexibility: It provides a way to define a family of behaviors within an object and change the behavior at runtime.
Encapsulation: Each state is encapsulated in its own class, which makes the code more maintainable and easier to understand.
Simplicity: It simplifies the code of the object that changes its behavior. Instead of having many conditional statements to determine what behavior to execute, the object can simply delegate this to the state object.
Open/Closed Principle: The State pattern adheres to the Open/Closed Principle as it allows introducing new states without changing the existing state classes or the context.
Localizing State-Specific Behavior: Each state class has all the behavior related to a particular state. This makes it easier to understand and modify the behavior associated with a particular state.
State Transitions: State transitions can be explicit and easy to understand because they are triggered by specific methods in the state classes.
Reducing State Transition Complexity: The pattern can also reduce the complexity of state transitions because the decision logic of the transition can be encapsulated within the state classes themselves.
8.5.1. Pattern
Changes based on class
Open/Close principle
Using polymorphism
8.5.2. Problem
Canvas object can behave differently depending on selected Tool
All behaviors are represented by subclass of the tool interface
from enum import Enum
class Tool(Enum):
SELECTION = 1
PENCIL = 2
ERASE = 3
BRUSH = 4
class Window:
current_tool: Tool
def on_left_mouse_button(self):
if self.current_tool == Tool.SELECTION:
print('Select')
elif self.current_tool == Tool.PENCIL:
print('Draw')
elif self.current_tool == Tool.ERASE:
print('Erase')
elif self.current_tool == Tool.BRUSH:
print('Paint')
def on_right_mouse_button(self):
if self.current_tool == Tool.SELECTION:
print('Unselect')
elif self.current_tool == Tool.PENCIL:
print('Stop drawing')
elif self.current_tool == Tool.ERASE:
print('Undo erase')
elif self.current_tool == Tool.BRUSH:
print('Stop painting')
if __name__ == '__main__':
window = Window()
window.current_tool = Tool.BRUSH
window.on_left_mouse_button()
window.on_right_mouse_button()
window.current_tool = Tool.SELECTION
window.on_left_mouse_button()
window.on_right_mouse_button()
window.current_tool = Tool.ERASE
window.on_left_mouse_button()
window.on_right_mouse_button()
# Paint
# Stop painting
# Select
# Unselect
# Erase
# Undo erase
8.5.3. Solution
from abc import ABC, abstractmethod
# %% Abstracts
class WindowEvents(ABC):
@abstractmethod
def on_left_mouse_button(self): ...
@abstractmethod
def on_right_mouse_button(self): ...
class Tool(WindowEvents, ABC):
pass
# %% Tools
class SelectionTool(Tool):
def on_left_mouse_button(self):
print('Select')
def on_right_mouse_button(self):
print('Unselect')
class EraseTool(Tool):
def on_left_mouse_button(self):
print('Erase')
def on_right_mouse_button(self):
print('Undo erase')
class PencilTool(Tool):
def on_left_mouse_button(self):
print('Draw')
def on_right_mouse_button(self):
print('Stop drawing')
class BrushTool(Tool):
def on_left_mouse_button(self):
print('Paint')
def on_right_mouse_button(self):
print('Stop painting')
# %% Main
class Window(WindowEvents):
current_tool: Tool
def on_left_mouse_button(self):
self.current_tool.on_left_mouse_button()
def on_right_mouse_button(self):
self.current_tool.on_right_mouse_button()
if __name__ == '__main__':
window = Window()
window.current_tool = BrushTool()
window.on_left_mouse_button()
window.on_right_mouse_button()
window.current_tool = SelectionTool()
window.on_left_mouse_button()
window.on_right_mouse_button()
window.current_tool = EraseTool()
window.on_left_mouse_button()
window.on_right_mouse_button()
# Paint
# Stop painting
# Select
# Unselect
# Erase
# Undo erase
8.5.4. Use Case - 1
>>> class Tool:
... def on_mouse_over(self): raise NotImplementedError
... def on_mouse_out(self): raise NotImplementedError
... def on_mouse_click_leftbutton(self): raise NotImplementedError
... def on_mouse_unclick_leftbutton(self): raise NotImplementedError
... def on_mouse_click_rightbutton(self): raise NotImplementedError
... def on_mouse_unclick_rightbutton(self): raise NotImplementedError
... def on_key_press(self): raise NotImplementedError
... def on_key_unpress(self): raise NotImplementedError
>>>
>>>
>>> class Pencil(Tool):
... def on_mouse_over(self):
... ...
...
... def on_mouse_out(self):
... ...
...
... def on_mouse_click_leftbutton(self):
... ...
...
... def on_mouse_unclick_leftbutton(self):
... ...
...
... def on_mouse_click_rightbutton(self):
... ...
...
... def on_mouse_unclick_rightbutton(self):
... ...
...
... def on_key_press(self):
... ...
...
... def on_key_unpress(self):
... ...
>>>
>>>
>>> class Pen(Tool):
... def on_mouse_over(self):
... ...
...
... def on_mouse_out(self):
... ...
...
... def on_mouse_click_leftbutton(self):
... ...
...
... def on_mouse_unclick_leftbutton(self):
... ...
...
... def on_mouse_click_rightbutton(self):
... ...
...
... def on_mouse_unclick_rightbutton(self):
... ...
...
... def on_key_press(self):
... ...
...
... def on_key_unpress(self):
... ...
8.5.5. References
8.5.6. Assignments
# %% License
# - Copyright 2025, Matt Harasymczuk <matt@python3.info>
# - This code can be used only for learning by humans
# - This code cannot be used for teaching others
# - This code cannot be used for teaching LLMs and AI algorithms
# - This code cannot be used in commercial or proprietary products
# - This code cannot be distributed in any form
# - This code cannot be changed in any form outside of training course
# - This code cannot have its license changed
# - If you use this code in your product, you must open-source it under GPLv2
# - Exception can be granted only by the author
# %% Run
# - PyCharm: right-click in the editor and `Run Doctest in ...`
# - PyCharm: keyboard shortcut `Control + Shift + F10`
# - Terminal: `python -m doctest -v myfile.py`
# %% About
# - Name: DesignPatterns Behavioral State
# - Difficulty: medium
# - Lines: 34
# - Minutes: 13
# %% English
# 1. Implement State pattern
# 2. Then add another language:
# - Chinese hello: 你好
# - Chinese goodbye: 再见
# 3. Run doctests - all must succeed
# %% Polish
# 1. Zaimplementuj wzorzec State
# 2. Następnie dodaj nowy język:
# - Chinese hello: 你好
# - Chinese goodbye: 再见
# 3. Uruchom doctesty - wszystkie muszą się powieść
# %% Tests
"""
>>> import sys; sys.tracebacklimit = 0
>>> assert sys.version_info >= (3, 9), \
'Python 3.9+ required'
>>> polish = Translation(Polish())
>>> english = Translation(English())
>>> chinese = Translation(Chinese())
>>> polish.hello()
'Cześć'
>>> polish.goodbye()
'Do widzenia'
>>> english.hello()
'Hello'
>>> english.goodbye()
'Goodbye'
>>> chinese.hello()
'你好'
>>> chinese.goodbye()
'再见'
"""
from enum import Enum
class Language(Enum):
POLISH = 'pl'
ENGLISH = 'en'
SPANISH = 'es'
class Translation:
language: Language
def __init__(self, language: Language):
self.language = language
def hello(self) -> str:
if self.language is Language.POLISH:
return 'Cześć'
elif self.language is Language.ENGLISH:
return 'Hello'
elif self.language is Language.SPANISH:
return 'Buenos Días'
else:
return 'Unknown language'
def goodbye(self) -> str:
if self.language is Language.POLISH:
return 'Do widzenia'
elif self.language is Language.ENGLISH:
return 'Goodbye'
elif self.language is Language.SPANISH:
return 'Adiós'
else:
return 'Unknown language'