Skip to content

Behavioral Design Patterns in Python

What Are Behavioral Patterns?

Behavioral patterns define common communication patterns between objects and realize these patterns. By doing so, these patterns increase flexibility in carrying out communication and help make complex control flows more understandable.

The 10 Behavioral Design Patterns

1. Chain of Responsibility

Purpose: Pass requests along a chain of handlers. Each handler decides either to process the request or to pass it to the next handler in the chain.

Python Example:

from abc import ABC, abstractmethod
from typing import Optional

class Handler(ABC):
    def __init__(self):
        self._next_handler: Optional[Handler] = None

    def set_next(self, handler: 'Handler') -> 'Handler':
        self._next_handler = handler
        return handler

    @abstractmethod
    def handle(self, request: str) -> Optional[str]:
        if self._next_handler:
            return self._next_handler.handle(request)
        return None

class BasicSupportHandler(Handler):
    def handle(self, request: str) -> Optional[str]:
        if "password reset" in request.lower():
            return f"BasicSupport: Handling '{request}'"
        return super().handle(request)

class TechnicalSupportHandler(Handler):
    def handle(self, request: str) -> Optional[str]:
        if "bug" in request.lower():
            return f"TechnicalSupport: Handling '{request}'"
        return super().handle(request)

class ManagerHandler(Handler):
    def handle(self, request: str) -> Optional[str]:
        if "refund" in request.lower():
            return f"Manager: Handling '{request}'"
        return super().handle(request)

# Usage
basic = BasicSupportHandler()
technical = TechnicalSupportHandler()
manager = ManagerHandler()

basic.set_next(technical).set_next(manager)

print(basic.handle("I need a password reset"))
print(basic.handle("I found a bug"))
print(basic.handle("I want a refund"))

2. Command

Purpose: Encapsulate a request as an object, thereby letting you parameterize clients with different requests, queue or log requests, and support undoable operations.

Python Example:

from abc import ABC, abstractmethod

class Command(ABC):
    @abstractmethod
    def execute(self) -> None:
        pass

    @abstractmethod
    def undo(self) -> None:
        pass

class Light:
    def turn_on(self):
        print("Light is ON")

    def turn_off(self):
        print("Light is OFF")

class LightOnCommand(Command):
    def __init__(self, light: Light):
        self._light = light

    def execute(self) -> None:
        self._light.turn_on()

    def undo(self) -> None:
        self._light.turn_off()

class RemoteControl:
    def __init__(self):
        self._history = []

    def execute_command(self, command: Command):
        command.execute()
        self._history.append(command)

    def undo_last(self):
        if self._history:
            command = self._history.pop()
            command.undo()

# Usage
light = Light()
light_on = LightOnCommand(light)
remote = RemoteControl()

remote.execute_command(light_on)  # Light is ON
remote.undo_last()  # Light is OFF

3. Iterator

Purpose: Provide a way to access elements of an aggregate object sequentially without exposing its underlying representation.

Python Example:

from collections.abc import Iterator, Iterable

class Book:
    def __init__(self, title: str, author: str):
        self.title = title
        self.author = author

class BookCollection(Iterable):
    def __init__(self):
        self._books = []

    def add_book(self, book: Book):
        self._books.append(book)

    def __iter__(self) -> Iterator:
        return BookIterator(self._books)

class BookIterator(Iterator):
    def __init__(self, books):
        self._books = books
        self._index = 0

    def __next__(self) -> Book:
        if self._index < len(self._books):
            book = self._books[self._index]
            self._index += 1
            return book
        raise StopIteration

# Usage
collection = BookCollection()
collection.add_book(Book("1984", "George Orwell"))
collection.add_book(Book("Brave New World", "Aldous Huxley"))

for book in collection:
    print(f"{book.title} by {book.author}")

4. Mediator

Purpose: Define an object that encapsulates how a set of objects interact, promoting loose coupling.

Python Example:

from abc import ABC, abstractmethod

class Mediator(ABC):
    @abstractmethod
    def notify(self, sender: object, event: str) -> None:
        pass

class ChatRoom(Mediator):
    def __init__(self):
        self._users = []

    def register_user(self, user: 'User'):
        self._users.append(user)

    def notify(self, sender: 'User', event: str) -> None:
        for user in self._users:
            if user != sender:
                user.receive(event)

class User:
    def __init__(self, name: str, mediator: Mediator):
        self._name = name
        self._mediator = mediator

    def send(self, message: str):
        print(f"{self._name} sends: {message}")
        self._mediator.notify(self, message)

    def receive(self, message: str):
        print(f"{self._name} receives: {message}")

# Usage
chat_room = ChatRoom()
alice = User("Alice", chat_room)
bob = User("Bob", chat_room)

chat_room.register_user(alice)
chat_room.register_user(bob)

alice.send("Hello, everyone!")

5. Memento

Purpose: Capture and externalize an object's internal state without violating encapsulation, allowing the object to be restored to this state later.

Python Example:

from typing import List
from datetime import datetime

class Memento:
    def __init__(self, state: str):
        self._state = state
        self._timestamp = datetime.now()

    def get_state(self) -> str:
        return self._state

    def get_timestamp(self) -> datetime:
        return self._timestamp

class TextEditor:
    def __init__(self):
        self._content = ""

    def write(self, text: str):
        self._content += text

    def save(self) -> Memento:
        return Memento(self._content)

    def restore(self, memento: Memento):
        self._content = memento.get_state()

    def show_content(self):
        print(f"Content: {self._content}")

class History:
    def __init__(self):
        self._mementos: List[Memento] = []

    def push(self, memento: Memento):
        self._mementos.append(memento)

    def pop(self) -> Memento:
        return self._mementos.pop()

# Usage
editor = TextEditor()
history = History()

editor.write("Hello ")
history.push(editor.save())

editor.write("World!")
editor.show_content()  # Hello World!

editor.restore(history.pop())
editor.show_content()  # Hello

6. Observer

Purpose: Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified.

Python Example:

from abc import ABC, abstractmethod
from typing import List

class Observer(ABC):
    @abstractmethod
    def update(self, subject: 'Subject') -> None:
        pass

class Subject:
    def __init__(self):
        self._observers: List[Observer] = []
        self._state = None

    def attach(self, observer: Observer):
        self._observers.append(observer)

    def detach(self, observer: Observer):
        self._observers.remove(observer)

    def notify(self):
        for observer in self._observers:
            observer.update(self)

    def set_state(self, state):
        self._state = state
        self.notify()

    def get_state(self):
        return self._state

class EmailAlert(Observer):
    def update(self, subject: Subject) -> None:
        print(f"EmailAlert: State changed to {subject.get_state()}")

class SMSAlert(Observer):
    def update(self, subject: Subject) -> None:
        print(f"SMSAlert: State changed to {subject.get_state()}")

# Usage
subject = Subject()
email = EmailAlert()
sms = SMSAlert()

subject.attach(email)
subject.attach(sms)

subject.set_state("New Product Available!")

7. State

Purpose: Allow an object to alter its behavior when its internal state changes.

Python Example:

from abc import ABC, abstractmethod

class State(ABC):
    @abstractmethod
    def handle(self, context: 'Context') -> None:
        pass

class DraftState(State):
    def handle(self, context: 'Context') -> None:
        print("Document is in Draft. Publishing...")
        context.set_state(PublishedState())

class PublishedState(State):
    def handle(self, context: 'Context') -> None:
        print("Document is Published. Archiving...")
        context.set_state(ArchivedState())

class ArchivedState(State):
    def handle(self, context: 'Context') -> None:
        print("Document is Archived. Cannot modify.")

class Context:
    def __init__(self):
        self._state = DraftState()

    def set_state(self, state: State):
        self._state = state

    def request(self):
        self._state.handle(self)

# Usage
doc = Context()
doc.request()  # Draft -> Published
doc.request()  # Published -> Archived
doc.request()  # Already Archived

8. Strategy

Purpose: Define a family of algorithms, encapsulate each one, and make them interchangeable.

Python Example:

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount: float) -> None:
        pass

class CreditCardPayment(PaymentStrategy):
    def __init__(self, card_number: str):
        self._card_number = card_number

    def pay(self, amount: float) -> None:
        print(f"Paid ${amount} using Credit Card ending in {self._card_number[-4:]}")

class PayPalPayment(PaymentStrategy):
    def __init__(self, email: str):
        self._email = email

    def pay(self, amount: float) -> None:
        print(f"Paid ${amount} using PayPal account {self._email}")

class CryptoPayment(PaymentStrategy):
    def __init__(self, wallet: str):
        self._wallet = wallet

    def pay(self, amount: float) -> None:
        print(f"Paid ${amount} using Crypto wallet {self._wallet}")

class ShoppingCart:
    def __init__(self):
        self._amount = 0
        self._payment_strategy = None

    def set_payment_strategy(self, strategy: PaymentStrategy):
        self._payment_strategy = strategy

    def set_amount(self, amount: float):
        self._amount = amount

    def checkout(self):
        if self._payment_strategy:
            self._payment_strategy.pay(self._amount)

# Usage
cart = ShoppingCart()
cart.set_amount(150.00)

cart.set_payment_strategy(CreditCardPayment("1234567812345678"))
cart.checkout()

cart.set_payment_strategy(PayPalPayment("user@example.com"))
cart.checkout()

9. Template Method

Purpose: Define the skeleton of an algorithm in a method, deferring some steps to subclasses.

Python Example:

from abc import ABC, abstractmethod

class DataMiner(ABC):
    def mine(self, path: str):
        file = self.open_file(path)
        data = self.extract_data(file)
        parsed_data = self.parse_data(data)
        analysis = self.analyze_data(parsed_data)
        self.send_report(analysis)
        self.close_file(file)

    @abstractmethod
    def open_file(self, path: str):
        pass

    @abstractmethod
    def extract_data(self, file):
        pass

    def parse_data(self, data):
        # Default implementation
        return data

    @abstractmethod
    def analyze_data(self, data):
        pass

    def send_report(self, analysis):
        print(f"Sending report: {analysis}")

    def close_file(self, file):
        print("File closed")

class CSVDataMiner(DataMiner):
    def open_file(self, path: str):
        print(f"Opening CSV file: {path}")
        return f"CSV_FILE:{path}"

    def extract_data(self, file):
        print("Extracting CSV data")
        return "CSV_DATA"

    def analyze_data(self, data):
        return "CSV Analysis Results"

class PDFDataMiner(DataMiner):
    def open_file(self, path: str):
        print(f"Opening PDF file: {path}")
        return f"PDF_FILE:{path}"

    def extract_data(self, file):
        print("Extracting PDF data")
        return "PDF_DATA"

    def analyze_data(self, data):
        return "PDF Analysis Results"

# Usage
csv_miner = CSVDataMiner()
csv_miner.mine("data.csv")

print("\\n")

pdf_miner = PDFDataMiner()
pdf_miner.mine("document.pdf")

10. Visitor

Purpose: Represent an operation to be performed on elements of an object structure, letting you define a new operation without changing the classes of the elements.

Python Example:

from abc import ABC, abstractmethod

class Visitor(ABC):
    @abstractmethod
    def visit_circle(self, circle: 'Circle') -> None:
        pass

    @abstractmethod
    def visit_rectangle(self, rectangle: 'Rectangle') -> None:
        pass

class Shape(ABC):
    @abstractmethod
    def accept(self, visitor: Visitor) -> None:
        pass

class Circle(Shape):
    def __init__(self, radius: float):
        self.radius = radius

    def accept(self, visitor: Visitor) -> None:
        visitor.visit_circle(self)

class Rectangle(Shape):
    def __init__(self, width: float, height: float):
        self.width = width
        self.height = height

    def accept(self, visitor: Visitor) -> None:
        visitor.visit_rectangle(self)

class AreaCalculator(Visitor):
    def visit_circle(self, circle: Circle) -> None:
        area = 3.14 * circle.radius ** 2
        print(f"Circle area: {area:.2f}")

    def visit_rectangle(self, rectangle: Rectangle) -> None:
        area = rectangle.width * rectangle.height
        print(f"Rectangle area: {area:.2f}")

class DrawVisitor(Visitor):
    def visit_circle(self, circle: Circle) -> None:
        print(f"Drawing circle with radius {circle.radius}")

    def visit_rectangle(self, rectangle: Rectangle) -> None:
        print(f"Drawing rectangle {rectangle.width}x{rectangle.height}")

# Usage
shapes = [Circle(5), Rectangle(4, 6), Circle(3)]

area_calculator = AreaCalculator()
drawer = DrawVisitor()

for shape in shapes:
    shape.accept(area_calculator)

print("\\n")

for shape in shapes:
    shape.accept(drawer)

When to Use Behavioral Patterns

  • Chain of Responsibility: When you want to decouple request senders from receivers
  • Command: When you need to parameterize objects with operations, queue operations, or implement undo/redo
  • Iterator: When you need to traverse a collection without exposing its internal structure
  • Mediator: When objects need to communicate but you want to avoid tight coupling
  • Memento: When you need to implement undo functionality or save/restore state
  • Observer: When changes to one object require changing others, and you don't know how many objects need to be changed
  • State: When an object's behavior depends on its state and it must change behavior at runtime
  • Strategy: When you have multiple algorithms and want to switch between them at runtime
  • Template Method: When you have an algorithm with invariant steps but some steps need customization
  • Visitor: When you need to perform operations across a diverse set of objects without modifying their classes

Conclusion

Behavioral design patterns are powerful tools in a Python developer's arsenal. They help manage complex control flows, improve code maintainability, and promote loose coupling between objects. By understanding and applying these patterns appropriately, you can write more elegant, flexible, and maintainable Python code.

Note

Patterns are not silver bullets. Use them when they solve real problems in your codebase, not just for the sake of using patterns.