Right, let’s get into the good stuff. Behavioral patterns are where we stop just building structures and start giving them actual brains. They’re about how objects talk to each other, who’s responsible for what, and how you manage complex flows of control without creating a spaghetti-code monster. These patterns are the difference between a codebase that works and one you can actually change without having a full-blown existential crisis.

Observer: Stop Manually Checking, Start Getting Updates

Ever found yourself writing a while True: loop that just checks and re-checks if some value has changed yet? You’re better than that. The Observer pattern is your way out. It sets up a one-to-many relationship: when one object (the “subject”) changes state, all its dependents (“observers”) are notified and updated automatically. It’s the core of event-driven programming.

Think of it like subscribing to a newsletter. You don’t refresh the news website every five minutes; you give them your email, and they send you the news when it’s ready. The subject is the newsletter company, and you are the observer.

Here’s how we do it without overcomplicating things. Notice I’m using abc for the base classes. This isn’t just pedantic OOP—it forces a clean contract.

from abc import ABC, abstractmethod

class Subject(ABC):
    """The one who has the news."""

    @abstractmethod
    def attach(self, observer):
        """Subscribe an observer."""
        pass

    @abstractmethod
    def detach(self, observer):
        """Unsubscribe an observer."""
        pass

    @abstractmethod
    def notify(self):
        """Send out the update to all subscribers."""
        pass

class ConcreteSubject(Subject):
    """The actual thing you're watching."""

    def __init__(self):
        self._observers = set()
        self._state = None  # This is the valuable data

    def attach(self, observer):
        self._observers.add(observer)

    def detach(self, observer):
        self._observers.discard(observer)

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

    @property
    def state(self):
        return self._state

    @state.setter
    def state(self, value):
        self._state = value
        self.notify()  # This is the magic. State changes? Everyone knows.

class Observer(ABC):
    """The subscriber."""

    @abstractmethod
    def update(self, subject):
        """Receive the update. The subject is passed for context."""
        pass

class ConcreteObserver(Observer):
    """An actual subscriber who does something with the news."""

    def update(self, subject):
        print(f"Observer got the news! New state is: {subject.state}")

# Let's see it in action
if __name__ == "__main__":
    subject = ConcreteSubject()
    observer_a = ConcreteObserver()
    observer_b = ConcreteObserver()

    subject.attach(observer_a)
    subject.attach(observer_b)

    subject.state = "First Update"  # Both observers will print
    subject.detach(observer_b)
    subject.state = "Second Update" # Only observer_a prints

Pitfall Alert: The order of notification is non-deterministic. If your observers have dependencies on each other, you’ve just created a race condition. Also, a poorly written observer that throws an exception can break the entire notification chain. Always wrap your observer.update() calls in try-catch blocks in a real system.

Strategy: Swap Algorithms Like Socks

This pattern is so stupidly simple and useful I’m almost angry I didn’t think of it first. The Strategy pattern defines a family of algorithms, encapsulates each one, and makes them interchangeable. It lets the algorithm vary independently from the client that uses it.

Translation: Stop using mile-long if/elif/else chains to pick behavior. Just define a common interface for your strategies and pass the one you want to the client.

from abc import ABC, abstractmethod
from typing import List

class SortStrategy(ABC):
    """The interface that all sorting strategies must implement."""
    @abstractmethod
    def sort(self, data: List) -> List:
        pass

class QuickSortStrategy(SortStrategy):
    def sort(self, data):
        # Let's pretend this is a real quicksort. You get the idea.
        return sorted(data)  # Python's sorted is Timsort, but for demo...

class ReverseSortStrategy(SortStrategy):
    def sort(self, data):
        return sorted(data, reverse=True)

class Sorter:
    """The Context class. It doesn't care which strategy it uses."""
    def __init__(self, strategy: SortStrategy):
        self._strategy = strategy

    def set_strategy(self, strategy: SortStrategy):
        """Allows changing the strategy at runtime."""
        self._strategy = strategy

    def execute_sort(self, data):
        """Delegates the work to the strategy object."""
        return self._strategy.sort(data)

# Usage
data = [5, 2, 8, 1]
sorter = Sorter(QuickSortStrategy())
print(sorter.execute_sort(data))  # [1, 2, 5, 8]

sorter.set_strategy(ReverseSortStrategy())
print(sorter.execute_sort(data))  # [8, 5, 2, 1]

Why this rules: You’ve just closed the Sorter class for modification (Open/Closed Principle). To add a new sorting algorithm, you create a new class. You don’t touch a single line of the Sorter code. This is how you avoid creating a 5000-line God class full of switches.

Command: Encapsulate All The Things

The Command pattern turns a request into a stand-alone object that contains all information about the request. This lets you parameterize methods with different requests, delay or queue a request’s execution, and support undoable operations.

It’s the patron saint of “I’ll get to it later.” You’re essentially creating a to-do list of command objects that can be executed whenever you feel like it.

from abc import ABC, abstractmethod

class Command(ABC):
    """The interface for all commands."""
    @abstractmethod
    def execute(self):
        pass

    # Optional but crucial for undo
    @abstractmethod
    def undo(self):
        pass

class LightOnCommand(Command):
    """A concrete command to turn a light on."""
    def __init__(self, light):
        self.light = light
        self.previous_state = None

    def execute(self):
        self.previous_state = self.light.state
        self.light.turn_on()

    def undo(self):
        if self.previous_state == "on":
            self.light.turn_on()
        else:
            self.light.turn_off()

class Light:
    """The Receiver class that knows how to do the actual work."""
    def __init__(self):
        self.state = "off"

    def turn_on(self):
        self.state = "on"
        print("Light is ON")

    def turn_off(self):
        self.state = "off"
        print("Light is OFF")

class RemoteControl:
    """The Invoker class. It holds a command and triggers it."""
    def __init__(self):
        self._command = None
        self.history = []  # For undo functionality

    def set_command(self, command):
        self._command = command

    def press_button(self):
        if self._command:
            self._command.execute()
            self.history.append(self._command)

    def press_undo(self):
        if self.history:
            last_command = self.history.pop()
            last_command.undo()

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

on_command = LightOnCommand(light)
remote.set_command(on_command)

remote.press_button()  # Light is ON
remote.press_undo()    # Light is OFF

The real power here is the Invoker (the remote) knows nothing about the Receiver (the light). It just knows how to call execute(). This decoupling is what allows you to build complex macro systems, queues, and undo stacks.

State: If Elif Elif Elif Elif…

You’ve seen it: a class with a state attribute and a method that’s just a horrifying chain of if self.state == 'A': ... elif self.state == 'B': .... The State pattern is the antidote. It allows an object to alter its behavior when its internal state changes. The object will appear to change its class.

You represent each state as a separate class, and the context object delegates all state-specific behavior to the current state object.

from abc import ABC, abstractmethod

class VendingMachineState(ABC):
    """Abstract state."""
    @abstractmethod
    def insert_money(self, amount):
        pass

    @abstractmethod
    def select_drink(self, drink):
        pass

    @abstractmethod
    def dispense(self):
        pass

class IdleState(VendingMachineState):
    def __init__(self, machine):
        self.machine = machine

    def insert_money(self, amount):
        print(f"${amount} inserted.")
        self.machine.balance = amount
        self.machine.set_state(HasMoneyState(self.machine))

    def select_drink(self, drink):
        print("Please insert money first.")

    def dispense(self):
        print("Please insert money first.")

class HasMoneyState(VendingMachineState):
    def __init__(self, machine):
        self.machine = machine

    def insert_money(self, amount):
        self.machine.balance += amount
        print(f"Balance: ${self.machine.balance}")

    def select_drink(self, drink):
        if drink.price <= self.machine.balance:
            print(f"Selected {drink.name}")
            self.machine.selected_drink = drink
            self.machine.set_state(DispenseState(self.machine))
        else:
            print("Not enough money.")

    def dispense(self):
        print("Please select a drink first.")

class DispenseState(VendingMachineState):
    def __init__(self, machine):
        self.machine = machine

    def insert_money(self, amount):
        print("Please wait, dispensing your drink.")

    def select_drink(self, drink):
        print("Please wait, dispensing your drink.")

    def dispense(self):
        print(f"Dispensing {self.machine.selected_drink.name}... Enjoy!")
        self.machine.balance -= self.machine.selected_drink.price
        # Give change, then return to idle
        self.machine.set_state(IdleState(self.machine))

class VendingMachine:
    """The Context."""
    def __init__(self):
        self.balance = 0
        self.selected_drink = None
        self._state = IdleState(self)

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

    def insert_money(self, amount):
        self._state.insert_money(amount)

    def select_drink(self, drink):
        self._state.select_drink(drink)

    def dispense(self):
        self._state.dispense()

# Usage
machine = VendingMachine()
machine.select_drink("Cola")  # "Please insert money first."
machine.insert_money(2.00)   # "$2.00 inserted."
machine.select_drink("Cola") # "Selected Cola" (assuming Cola costs <= $2)
machine.dispense()           # "Dispensing Cola... Enjoy!"

The beauty is that adding a new state, like an OutOfOrderState, is trivial. You never have to touch the logic of the other states. You just create a new class and make the context transition to it. It turns a tangled web of conditional logic into a clean, organized set of objects.

Template Method: The Skeleton Key

This one is about keeping your DRY principle intact. The Template Method pattern defines the skeleton of an algorithm in a method, deferring some steps to subclasses. It lets subclasses redefine certain steps of an algorithm without changing the algorithm’s structure.

It’s the “fill-in-the-blanks” approach to design. The parent class says “here’s the order things happen in, you handle the specifics.”

from abc import ABC, abstractmethod

class DataExporter(ABC):
    """This class defines the algorithm's skeleton."""

    def export(self, data):
        """The Template Method. It's final for a reason."""
        self.validate_data(data)
        cleaned_data = self.clean_data(data)
        formatted_data = self.format_data(cleaned_data)
        return self.send_data(formatted_data)

    def validate_data(self, data):
        # A common step with a default implementation
        if not data:
            raise ValueError("Data cannot be empty.")

    def clean_data(self, data):
        # Another common step
        return [item for item in data if item is not None]

    @abstractmethod
    def format_data(self, data):
        """Subclasses MUST implement this."""
        pass

    @abstractmethod
    def send_data(self, data):
        """Subclasses MUST implement this."""
        pass

class CSVExporter(DataExporter):
    def format_data(self, data):
        return ",".join(map(str, data))

    def send_data(self, data):
        print(f"Pretending to write '{data}' to a CSV file.")
        return True

class JSONExporter(DataExporter):
    def format_data(self, data):
        import json
        return json.dumps(data)

    def send_data(self, data):
        print(f"Pretending to POST '{data}' to a web API.")
        return True

# Usage
data = [1, 2, 3, None, 4]
csv_exporter = CSVExporter()
csv_exporter.export(data)  # Validates, cleans, formats as CSV, "sends"

json_exporter = JSONExporter()
json_exporter.export(data) # Validates, cleans, formats as JSON, "sends"

The key here is the non-abstract methods in the base class. They provide common, reusable functionality that all exporters share. The subclasses only worry about the bits that are actually different. This is how you prevent copy-pasting the validate_data and clean_data logic into every single exporter class. It centralizes the algorithm’s flow and maximizes code reuse. It’s relentlessly pragmatic.