Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes
Sponsored Reserved space — layout preview until AdSense is connected

Python

Why You're Already Using Design Patterns (And Don't Even Know It)

Discover how Python's built-in features naturally implement classic design patterns like Singleton, Factory, Observer, and Strategy - often without you realizing it. Learn practical, Pythonic alternatives to textbook pattern implementations.

June 2026 · 8 min read · 1 views · 0 hearts

Why You're Already Using Design Patterns (And Don't Even Know It)

You've probably written a factory function without realizing it's a "design pattern." You've definitely used a singleton when you imported a module. The truth is, Python developers stumble into patterns naturally because the language itself encourages certain solutions.

Let's cut through the textbook definitions and look at patterns you'll actually use, with real code you can run today.

The Singleton You've Used Since Day One

Python modules themselves are singletons. When you write:

import config
from config import API_KEY

The module config is loaded once and cached. This is the Singleton pattern — ensuring a class has only one instance — baked into Python's import system.

But when you need a true singleton class? Don't write the usual __new__ boilerplate. Use a module-level instance instead:

# database.py
class DatabaseConnection:
    def __init__(self):
        self.connected = False

    def connect(self):
        self.connected = True

db = DatabaseConnection()  # This IS your singleton

Any module importing db gets the same instance. Simple, Pythonic, zero magic.

The Factory Pattern Without the Ceremony

Java factories often involve abstract classes and interfaces. In Python, just use a function:

def document_parser(filepath):
    if filepath.endswith('.pdf'):
        return PDFParser(filepath)
    elif filepath.endswith('.csv'):
        return CSVParser(filepath)
    elif filepath.endswith('.json'):
        return JSONParser(filepath)
    else:
        raise ValueError(f"Unsupported format: {filepath}")

parser = document_parser("report.csv")

This is a Factory pattern — and it's clearer than a class-based version. Need more structure? Add a dictionary lookup:

PARSERS = {
    '.pdf': PDFParser,
    '.csv': CSVParser,
    '.json': JSONParser,
}

parser = PARSERS[extension](filepath)

The Observer Pattern with Python's Built-ins

Event-driven code? Don't write an observer framework. Use callable objects and lists:

class NewsPublisher:
    def __init__(self):
        self.subscribers = []

    def subscribe(self, callback):
        self.subscribers.append(callback)

    def publish(self, article):
        for subscriber in self.subscribers:
            subscriber(article)

publisher = NewsPublisher()
publisher.subscribe(lambda msg: print(f"Email: {msg}"))
publisher.subscribe(lambda msg: print(f"Slack: {msg}"))
publisher.publish("New release tomorrow!")

Notice this works without inheritance or abstract interfaces. Python's duck typing means any callable object works.

Strategy Pattern with Functions

When you need interchangeable algorithms, reach for functions or classes with a common method:

def priority_strategy(tasks):
    return sorted(tasks, key=lambda t: t.priority, reverse=True)

def deadline_strategy(tasks):
    return sorted(tasks, key=lambda t: t.deadline)

class TaskScheduler:
    def __init__(self, strategy):
        self.strategy = strategy

    def schedule(self, tasks):
        return self.strategy(tasks)

scheduler = TaskScheduler(priority_strategy)
print(scheduler.schedule(my_tasks))

Need to switch strategies? Just pass a different function:

scheduler.strategy = deadline_strategy

The Decorator Pattern (Language Support Edition)

Python's @decorator syntax is literally the Decorator pattern — adding behavior to an object without modifying it:

import time
from functools import wraps

def log_elapsed(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__} took {elapsed:.3f}s")
        return result
    return wrapper

@log_elapsed
def data_pipeline():
    time.sleep(1.5)
    return "done"

data_pipeline()  # prints: data_pipeline took 1.500s

The @wraps decorator preserves the original function's metadata. Without it, you'd lose the function name and docstring.

When Patterns Backfire in Python

Some traditional patterns cause more pain than gain in Python:

  • Abstract Factory: Often just over-engineering. Use dictionaries and functions.
  • Builder: The builder pattern makes sense when you need immutable objects with many parameters. In Python, you can often just use keyword arguments with sensible defaults.
  • Visitor: This pattern exists to work around languages without multiple dispatch. Python has functools.singledispatch which handles this more cleanly.

Real-World Example: Combining Patterns

Here's a practical scenario that uses three patterns together. A logging system that can format messages different ways (Strategy) and notify multiple handlers (Observer), with a shared configuration (Singleton):

import logging

class LoggerConfig:
    """Module-level singleton via normal import"""
    pass

def json_formatter(message):
    return f'{{"msg": "{message}"}}'

def plain_formatter(message):
    return message

class Logger:
    def __init__(self, name, formatter=plain_formatter):
        self.name = name
        self.formatter = formatter
        self.handlers = []  # Observer pattern

    def add_handler(self, handler):
        self.handlers.append(handler)

    def log(self, message):
        formatted = self.formatter(f"{self.name}: {message}")
        for handler in self.handlers:
            handler(formatted)

# Usage
logger = Logger("app", json_formatter)
logger.add_handler(print)
logger.add_handler(lambda m: open("debug.log", "a").write(m + "\n"))
logger.log("user logged in")

This code is production-ready, testable, and extends naturally. Each pattern serves a purpose without adding ceremony.

The Smart Way to Use Patterns

The best pattern is the one you don't notice. Python's readability lets you implement patterns with 10 lines of code where Java might need 50. Always prefer:

  1. A plain function over a class with one method
  2. A module-level variable over a singleton class
  3. A list of callbacks over an event system

When you recognize you're solving a common problem, reach for the simplest Pythonic expression of that pattern. Your future self — and anyone reading your code — will thank you.

Comments

Questions, corrections, and tips stay visible for everyone reading this page.

0 in thread

Join the discussion

Shown next to your comment.

Up to 4,000 characters

No comments yet

Be the first to leave a note — it helps the next reader.