Alright, let’s talk about the structural patterns. These are the blueprints for how you compose your objects and classes into larger structures without ending up with a tangled mess that keeps you up at night. They’re less about creating objects (that’s the creational gang’s job) and more about making sure the objects you do have can work together without driving each other insane.

The Adapter: Making the Incompatible Play Nice

You know that feeling when you have a brilliant, high-powered USB-C device and all you can find is an ancient USB-A port? You need an adapter. That’s this pattern. You have a class (Client) that expects to talk to a specific interface (Target), and you have another class (Adaptee) that does what you need but speaks a completely different language. The Adapter wraps the Adaptee and translates the Client’s requests into something it understands.

Why not just change the Adaptee? Sometimes you can’t. It’s from a third-party library, or it’s legacy code that’s too terrifying to touch. The Adapter lets you introduce new functionality without blowing up the existing system.

# What the Client expects to work with
class EuropeanSocket:
    def voltage(self): return 230
    def live(self): return 1
    def neutral(self): return 2
    def earth(self): return 3

# The incompatible thing you desperately need to use (US Socket)
class USPlug:
    def voltage(self): return 120
    def live(self): return 'black'
    def neutral(self): return 'white'
    # Notice: no earth() method! A "questionable choice," indeed.

# The Adapter to the rescue
class USPlugToEuropeanSocketAdapter:
    def __init__(self, us_plug):
        self._us_plug = us_plug

    def voltage(self):
        return self._us_plug.voltage()  # This might require a transformer too!

    def live(self):
        # Translate the color to the pin number
        return self._us_plug.live()

    def neutral(self):
        return self._us_plug.neutral()

    def earth(self):
        # The US plug has no earth, so we have to make a choice.
        # This is a common pitfall: imperfect adaptation.
        # Best practice: Be explicit about the compromise.
        raise NotImplementedError("This US plug is not grounded. Don't use it with your fancy stereo.")

# Client code remains blissfully unaware it's talking to an American
euro_socket = EuropeanSocket()
try:
    print(f"Voltage: {euro_socket.voltage()}")
except:
    pass

us_plug = USPlug()
adapter = USPlugToEuropeanSocketAdapter(us_plug)
print(f"Adapted Voltage: {adapter.voltage()}")
# adapter.earth()  # This will raise the NotImplementedError. Safety first.

The Decorator: The Power-Up of Composition

Forget inheritance. It’s a rigid, parental “you are this” relationship. The Decorator pattern is a cool, compositional “you have these abilities” relationship. You want to add responsibilities to an object dynamically, without affecting other objects of the same class. It’s like wrapping a gift. You can add a box, then paper, then a bow. Each wrapper is-a gift (it has the same interface) but it also has-a gift it’s decorating.

Why is this brilliant? Because you can mix and match behaviors at runtime, and you avoid the dreaded “class explosion” of a BorderedScrollableLockableTextView subclass.

# The core component interface
class Coffee:
    def cost(self):
        pass

# A concrete component
class SimpleCoffee(Coffee):
    def cost(self):
        return 5  # base price

# The base decorator class follows the same interface as Coffee.
# This is the key. It's a coffee, and it *has* a coffee.
class CoffeeDecorator(Coffee):
    def __init__(self, decorated_coffee):
        self._decorated_coffee = decorated_coffee

    def cost(self):
        return self._decorated_coffee.cost()

# Concrete decorators
class WithMilk(CoffeeDecorator):
    def cost(self):
        # Add cost of milk to the *wrapped* coffee's cost
        return super().cost() + 1.5

class WithVanilla(CoffeeDecorator):
    def cost(self):
        return super().cost() + 2.0

# Let's build a drink
my_coffee = SimpleCoffee()
print(f"Simple coffee: ${my_coffee.cost()}")

my_fancy_coffee = WithVanilla(WithMilk(my_coffee))
# It's a WithVanilla, which wraps a WithMilk, which wraps a SimpleCoffee.
print(f"Vanilla latte: ${my_fancy_coffee.cost()}") # Output: 5 + 1.5 + 2.0 = 8.5

The Proxy: The Gatekeeper

A Proxy stands in for another object, controlling access to it. The genius is that the caller shouldn’t know if it’s talking to the real object or the proxy. Why would you do this?

  • Lazy Initialization (Virtual Proxy): That real object might be huge and expensive to create. The proxy waits until the last possible second to create it.
  • Access Control (Protection Proxy): The proxy checks credentials before letting you call the real object.
  • Logging (Logging Proxy): The proxy records every method call before delegating.
class ExpensiveDatabaseService:
    """This object is very costly to instantiate."""
    def __init__(self):
        print("Oh no... initializing the expensive database connection!")
        # This is where the 10-second network call would happen.

    def fetch_data(self, query):
        return f"Results for '{query}'"

class LazyProxy:
    def __init__(self):
        self._service = None  # We don't create it yet!

    def fetch_data(self, query):
        if self._service is None:
            self._service = ExpensiveDatabaseService()  # Created on first use!
        return self._service.fetch_data(query)

# Client code
proxy = LazyProxy()
print("Proxy created, but the expensive service hasn't been.")
# ... time passes ...
print(proxy.fetch_data("SELECT * FROM jokes")) # Now it gets created.

The Facade: Your Project’s Welcome Mat

A system has a dozen complex subsystems with gnarly interfaces. You don’t want your client code to know about all of them. The Facade pattern provides a simple, unified interface to that complex subsystem. It’s not about encapsulating the subsystem; it’s about hiding it. It’s the friendly receptionist who handles the messy internal routing for you.

Think of it as the requests library. You call requests.get(url). You don’t manually manage sockets, HTTP headers, SSL certificates, and connection pooling. That’s the facade’s job.

# Complex, low-level subsystems you don't want to deal with
class InvoiceSystem:
    def create_invoice(self): return "Invoice created"

class ShippingSystem:
    def schedule_pickup(self, address): return f"Pickup scheduled for {address}"

class NotificationSystem:
    def send_email(self, to, message): return f"Email to {to}: {message}"

# The Facade: a simple API for a complex process
class OrderFulfillmentFacade:
    def __init__(self):
        self._invoice = InvoiceSystem()
        self._shipping = ShippingSystem()
        self._notify = NotificationSystem()

    def process_order(self, product, address, email):
        """The one simple method you actually need."""
        results = []
        results.append(self._invoice.create_invoice())
        results.append(self._shipping.schedule_pickup(address))
        results.append(self._notify.send_email(email, "Your order shipped!"))
        return results

# Client code is now clean and semantic
facade = OrderFulfillmentFacade()
print(facade.process_order("Python Book", "123 Main St", "reader@example.com"))

The Composite: Treating Collections and Individuals Alike

This one is a mind-bender with an elegantly simple goal. The Composite pattern lets you treat a single object and a group (composite) of objects uniformly. You build a tree structure where every node is either a Leaf (the individual object) or a Composite (a node that contains other nodes, which can be Leaves or other Composites).

The classic example is a file system. A File is a Leaf. A Directory is a Composite—it has children which can be Files or other Directories. Both File and Directory implement the same FileSystemNode interface with a get_size() method.

# The common component interface
class FileSystemComponent:
    def get_size(self):
        pass

# The Leaf
class File(FileSystemComponent):
    def __init__(self, size):
        self._size = size

    def get_size(self):
        return self._size

# The Composite
class Directory(FileSystemComponent):
    def __init__(self):
        self._children = []

    def add(self, component):
        self._children.append(component)

    def remove(self, component):
        self._children.remove(component)

    def get_size(self):
        # This is the magic. It delegates to all its children.
        total = 0
        for child in self._children:
            total += child.get_size()  # Works for both Files and Directories!
        return total

# Building a filesystem tree
root_dir = Directory()
bin_dir = Directory()
home_dir = Directory()

root_dir.add(bin_dir)
root_dir.add(home_dir)

bin_dir.add(File(150000))  # /bin/ls
home_dir.add(File(2500))   # /home/user/.bashrc

user_dir = Directory()
home_dir.add(user_dir)
user_dir.add(File(500))    # /home/user/notes.txt

print(f"Total size of root: {root_dir.get_size()} bytes") # 150000 + 2500 + 500

The power here is that the client code can call get_size() on any node in the tree without knowing or caring if it’s a file or a directory. The composite handles the recursion for you. The pitfall? You have to be careful about circular references, or your get_size() call will never end.