Skip to Content
Course content

379: Choosing Patterns Pythonically

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I see this all the time with developers moving from Java or C# into Python: they arrive with a deep knowledge of the "Gang of Four" design patterns and try to implement them with surgical precision. They build elaborate class hierarchies, abstract base classes, and strict interfaces because that's how they were taught to write "professional" code. The misconception is that a design pattern is a rigid blueprint that must be mirrored exactly in every language to be effective.

In Python, following those blueprints too literally often leads to "Enterprise Java Syndrome"—code that is bloated, difficult to read, and fights against the language's nature. Python is dynamic. It treats functions as first-class objects. When you try to force a rigid GoF pattern onto Python, you're usually adding boilerplate that the language has already solved internally.

The Strategy Pattern Trap vs. First-Class Functions

Take the Strategy Pattern. In a strictly typed language, you'd create an interface (e.g., PaymentStrategy), implement several concrete classes (CreditCardPayment, PayPalPayment), and then create a context class to hold a reference to the strategy. It's a lot of typing for something very simple.

Here is how that looks when someone is "over-patterning" in Python:

from abc import ABC, abstractmethod

class PaymentStrategy(ABC):
    @abstractmethod
    def pay(self, amount):
        pass

class CreditCardPayment(PaymentStrategy):
    def pay(self, amount):
        print(f"Paying {amount} using Credit Card")

class PayPalPayment(PaymentStrategy):
    def pay(self, amount):
        print(f"Paying {amount} using PayPal")

class Order:
    def __init__(self, amount, strategy: PaymentStrategy):
        self.amount = amount
        self.strategy = strategy

    def process(self):
        self.strategy.pay(self.amount)

# Usage
order = Order(100, PayPalPayment())
order.process()

Now, let's be honest: that's a lot of scaffolding. In Python, a "strategy" is just a callable. You don't need a class hierarchy if the "strategy" doesn't need to maintain complex internal state. You can just pass the function.

def pay_with_credit_card(amount):
    print(f"Paying {amount} using Credit Card")

def pay_with_paypal(amount):
    print(f"Paying {amount} using PayPal")

class Order:
    def __init__(self, amount, payment_func):
        self.amount = amount
        self.payment_func = payment_func

    def process(self):
        self.payment_func(self.amount)

# Usage
order = Order(100, pay_with_paypal)
order.process()

I prefer the second approach every time. It's leaner, faster to write, and easier to test. You've achieved the exact same architectural goal—decoupling the payment logic from the order logic—without the ceremony.

The Singleton Obsession vs. Module-Level State

Another classic is the Singleton. People spend hours debating the "best" way to implement a Singleton in Python, using __new__ or metaclasses to ensure only one instance of a class ever exists. They're treating the class as a container for global state.

But here is the secret: Modules in Python are already Singletons.

If you need a single point of configuration or a shared resource (like a database connection pool) across your entire app, don't build a DatabaseConnectionManager class with a get_instance() method. Just put the instance in a module.

# db_config.py
class DatabaseConnection:
    def __init__(self):
        self.connected = False
    
    def connect(self):
        self.connected = True
        print("Connected to DB")

# We instantiate it right here in the module
connection = DatabaseConnection()

Now, anywhere else in your project, you just do from db_config import connection. Python's import machinery ensures that the module is executed only once. Every other file that imports connection gets the exact same object. You get the Singleton pattern for free, without the confusing metaclass magic.

The rule of thumb I use is this: if you find yourself writing a class that only ever has one instance, or a class that only contains one method, stop. You're probably trying to solve a Python problem with a Java solution. Look for a way to use a function, a dictionary, or a module instead.




📋 Practical Task

Refactoring a Rigid File-Exporter to a Pythonic Registry

You have been handed a piece of legacy code that uses a rigid "Exporter" class hierarchy. The current implementation is over-engineered and makes adding new export formats tedious. Your task is to refactor this into a Pythonic "Registry" pattern using a dictionary of functions.

Current Rigid Implementation:

from abc import ABC, abstractmethod

class BaseExporter(ABC):
    @abstractmethod
    def export(self, data):
        pass

class JSONExporter(BaseExporter):
    def export(self, data):
        return f"Exporting {data} as JSON"

class CSVExporter(BaseExporter):
    def export(self, data):
        return f"Exporting {data} as CSV"

class ExportManager:
    def __init__(self, exporter: BaseExporter):
        self.exporter = exporter
    
    def run(self, data):
        return self.exporter.export(data)

Requirements for your refactor:

  • Remove all ABC and class-based exporter logic.
  • Create standalone functions for export_json and export_csv.
  • Create a dictionary called EXPORT_REGISTRY that maps strings (e.g., "json", "csv") to these functions.
  • Implement a single function handle_export(format_type, data) that looks up the correct function in the registry and executes it.
  • Include a check to handle cases where an unsupported format_type is requested (e.g., raise a ValueError).
Rating
0 0

There are no comments for now.

to be the first to leave a comment.