OOP Design Patterns in Python

advanced design-patterns singleton factory observer strategy decorator iterator

Design patterns are proven solutions to common problems. But Python’s flexibility means we often implement them differently than Java or C++. First-class functions, decorators, and duck typing let us skip a lot of the ceremony. Here are the patterns that come up most in Python.

Singleton — One Instance Only

A singleton ensures only one instance of a class exists. In Python, the simplest approach is just a module-level instance. But here’s the __new__ approach for when we need a class:

class Database:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.connection = "connected"  # init once
        return cls._instance

db1 = Database()
db2 = Database()
print(db1 is db2)  # True — same object

The Pythonic way: just use a module. A module is imported once, so module-level variables are natural singletons.

# config.py
settings = {"debug": True, "db_url": "postgres://..."}
# everywhere else: from config import settings

Factory — @classmethod Factories

Factory methods create objects without exposing the creation logic. In Python, @classmethod is the natural fit.

class User:
    def __init__(self, name: str, role: str):
        self.name = name
        self.role = role

    @classmethod
    def admin(cls, name: str) -> "User":
        return cls(name, role="admin")

    @classmethod
    def from_dict(cls, data: dict) -> "User":
        return cls(data["name"], data["role"])

admin = User.admin("Manish")
user = User.from_dict({"name": "Raj", "role": "viewer"})
print(admin.role)  # admin

We get named constructors that clearly communicate intent — much better than passing flags to __init__.

Builder — Fluent Method Chaining

The builder pattern constructs complex objects step by step. In Python, we return self from each method to enable chaining.

class QueryBuilder:
    def __init__(self):
        self._table = ""
        self._conditions = []
        self._limit = None

    def table(self, name: str) -> "QueryBuilder":
        self._table = name
        return self  # return self for chaining

    def where(self, condition: str) -> "QueryBuilder":
        self._conditions.append(condition)
        return self

    def limit(self, n: int) -> "QueryBuilder":
        self._limit = n
        return self

    def build(self) -> str:
        query = f"SELECT * FROM {self._table}"
        if self._conditions:
            query += " WHERE " + " AND ".join(self._conditions)
        if self._limit:
            query += f" LIMIT {self._limit}"
        return query

sql = QueryBuilder().table("users").where("age > 18").where("active = 1").limit(10).build()
print(sql)  # SELECT * FROM users WHERE age > 18 AND active = 1 LIMIT 10

Observer — Event System with Callbacks

The observer pattern lets objects subscribe to events on another object. In Python, we use simple callback lists.

class EventEmitter:
    def __init__(self):
        self._listeners = {}  # event_name -> list of callbacks

    def on(self, event: str, callback):
        self._listeners.setdefault(event, []).append(callback)

    def emit(self, event: str, *args):
        for cb in self._listeners.get(event, []):
            cb(*args)

emitter = EventEmitter()
emitter.on("user_created", lambda name: print(f"Welcome, {name}!"))
emitter.on("user_created", lambda name: print(f"Sending email to {name}"))
emitter.emit("user_created", "Manish")
# Welcome, Manish!
# Sending email to Manish

Because functions are first-class in Python, we don’t need separate Observer and Subject interfaces. A callback is enough.

Strategy — Functions as Strategies

The strategy pattern swaps algorithms at runtime. In Java, this means interfaces and classes. In Python, we just pass functions.

def bubble_sort(data: list) -> list:
    items = data[:]
    for i in range(len(items)):
        for j in range(len(items) - 1 - i):
            if items[j] > items[j + 1]:
                items[j], items[j + 1] = items[j + 1], items[j]
    return items

def builtin_sort(data: list) -> list:
    return sorted(data)

class Sorter:
    def __init__(self, strategy=builtin_sort):  # default strategy
        self.strategy = strategy

    def sort(self, data: list) -> list:
        return self.strategy(data)

s = Sorter(strategy=bubble_sort)  # swap strategy at construction
print(s.sort([3, 1, 2]))  # [1, 2, 3]

The only difference from the classic pattern is that we pass a function instead of an object. Python’s first-class functions make the pattern almost invisible.

Decorator — Function and Class-Based

The decorator pattern wraps an object to add behavior. Python has it baked into the language with @decorator syntax.

Function decorator (the most common form):

import time

def timer(func):
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        print(f"{func.__name__} took {time.perf_counter() - start:.4f}s")
        return result
    return wrapper

@timer
def slow_function():
    time.sleep(0.1)

slow_function()  # slow_function took 0.1002s

Class-based decorator (when we need state):

class CountCalls:
    def __init__(self, func):
        self.func = func
        self.count = 0

    def __call__(self, *args, **kwargs):
        self.count += 1
        print(f"Call #{self.count}")
        return self.func(*args, **kwargs)

@CountCalls
def greet(name):
    return f"Hello, {name}!"

greet("Manish")  # Call #1 → Hello, Manish!
greet("Raj")     # Call #2 → Hello, Raj!
print(greet.count)  # 2 — state is preserved

Iterator — __iter__ / __next__ and Generators

The iterator pattern provides a way to traverse a collection without exposing its internal structure. Python’s iterator protocol is built on two dunder methods.

class Countdown:
    def __init__(self, start):
        self.current = start

    def __iter__(self):
        return self  # the object is its own iterator

    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        val = self.current
        self.current -= 1
        return val

for num in Countdown(3):
    print(num)  # 3, 2, 1

The Pythonic way: generators do the same thing with way less code.

def countdown(start):
    while start > 0:
        yield start  # pauses here, resumes on next iteration
        start -= 1

for num in countdown(3):
    print(num)  # 3, 2, 1

Generators are lazy iterators built into the language. For most cases, they replace the need for a full iterator class.

Template Method — ABC with Hook Methods

The template method defines the skeleton of an algorithm in a base class, letting subclasses fill in specific steps. Think of it like a form with blanks to fill in.

from abc import ABC, abstractmethod

class DataPipeline(ABC):
    def run(self):  # template method — defines the steps
        data = self.extract()
        cleaned = self.transform(data)
        self.load(cleaned)

    @abstractmethod
    def extract(self) -> list: ...

    @abstractmethod
    def transform(self, data: list) -> list: ...

    @abstractmethod
    def load(self, data: list) -> None: ...

class CsvPipeline(DataPipeline):
    def extract(self) -> list:
        return ["raw1", "raw2"]  # read from CSV

    def transform(self, data: list) -> list:
        return [d.upper() for d in data]  # clean data

    def load(self, data: list) -> None:
        print(f"Loaded: {data}")  # save to DB

CsvPipeline().run()  # Loaded: ['RAW1', 'RAW2']

The run() method is the template. It calls extract, transform, and load in order. Subclasses provide the concrete implementations, but the overall flow stays the same.

Which Pattern, When?

  • Singleton — configuration, database connections, caches (but prefer modules)
  • Factory — multiple ways to create an object (from_json, from_csv, admin)
  • Builder — constructing complex objects step by step (queries, configs)
  • Observer — event-driven systems, pub/sub, UI updates
  • Strategy — swappable algorithms (sorting, validation, pricing rules)
  • Decorator — adding behavior without modifying the original (logging, timing, auth)
  • Iterator — traversing collections lazily (prefer generators)
  • Template Method — fixed algorithm with customizable steps (ETL, tests, workflows)

In simple language, design patterns are reusable solutions to common problems. Python’s features — first-class functions, decorators, generators, duck typing — let us implement these patterns with far less boilerplate than traditional OOP languages. The pattern is still there; the ceremony isn’t.