Course topics

By WebNest Studio

Python Tutorial

Design Patterns in Python

Design patterns are proven solutions to recurring design problems, catalogued by the "Gang of Four" in 1994: creating objects flexibly, structuring relationships, and organising behaviour. Many patterns look different — often simpler — in Python, because functions are first-class objects, modules are natural singletons, and duck typing removes the need for many interfaces.

This lesson implements the most useful patterns the Pythonic way: Singleton, Factory, Builder, Adapter, Decorator, Facade, Strategy, Observer and Command, each with a runnable example.

Creational Patterns

Singleton ensures one instance — in Python a module-level object or a cached function is usually enough. Factory creates objects without the caller knowing the concrete class — often a dict mapping names to classes. Builder assembles complex objects step by step, often with method chaining.

Structural Patterns

Adapter wraps an incompatible interface to match the one your code expects. Decorator adds behaviour to objects or functions without changing them — Python's @decorator syntax implements the function version directly. Facade provides one simple interface to a complex subsystem.

Behavioural Patterns

Strategy selects an algorithm at runtime — in Python, just pass a function. Observer notifies subscribers when something happens (event systems, signals). Command wraps an action as an object so it can be queued, logged or undone. Iterator is built into the language through __iter__ and generators.

Use Patterns Sparingly

Patterns are a vocabulary, not a checklist. Apply one when it removes real duplication or coupling; otherwise the simplest code wins.

Examples

Singleton, Factory and Builder

Python
from functools import cache

@cache
def get_settings():                 # Pythonic singleton: created once, then reused
    print("loading settings")
    return {"currency": "INR"}

print(get_settings() is get_settings())

class PdfExporter:
    def export(self, data): return f"PDF({len(data)} rows)"
class CsvExporter:
    def export(self, data): return f"CSV({len(data)} rows)"

EXPORTERS = {"pdf": PdfExporter, "csv": CsvExporter}

def make_exporter(kind):            # Factory
    try:
        return EXPORTERS[kind]()
    except KeyError:
        raise ValueError(f"unknown format {kind!r}") from None

print(make_exporter("csv").export([1, 2, 3]))

class QueryBuilder:                 # Builder with method chaining
    def __init__(self, table):
        self.table, self.filters, self.limit_n = table, [], None
    def where(self, condition):
        self.filters.append(condition)
        return self
    def limit(self, n):
        self.limit_n = n
        return self
    def build(self):
        sql = f"SELECT * FROM {self.table}"
        if self.filters:
            sql += " WHERE " + " AND ".join(self.filters)
        if self.limit_n:
            sql += f" LIMIT {self.limit_n}"
        return sql

print(QueryBuilder("students").where("age > 18").where("city = 'Pune'").limit(10).build())
Output
loading settings
True
CSV(3 rows)
SELECT * FROM students WHERE age > 18 AND city = 'Pune' LIMIT 10

Adapter, Decorator and Facade

Python
import functools

class LegacyPaymentApi:              # incompatible interface
    def make_payment(self, paise):
        return f"legacy paid {paise} paise"

class PaymentAdapter:                # Adapter
    def __init__(self, legacy):
        self.legacy = legacy
    def pay(self, rupees):
        return self.legacy.make_payment(int(rupees * 100))

print(PaymentAdapter(LegacyPaymentApi()).pay(49.5))

def logged(func):                    # Decorator
    @functools.wraps(func)
    def wrapper(*args):
        result = func(*args)
        print(f"{func.__name__}{args} -> {result}")
        return result
    return wrapper

@logged
def add(a, b):
    return a + b
add(2, 3)

class Inventory:
    def reserve(self, item): return f"reserved {item}"
class Billing:
    def charge(self, amount): return f"charged {amount}"
class Shipping:
    def ship(self, item): return f"shipped {item}"

class CheckoutFacade:                # Facade
    def __init__(self):
        self.inv, self.bill, self.ship = Inventory(), Billing(), Shipping()
    def place_order(self, item, amount):
        return [self.inv.reserve(item), self.bill.charge(amount), self.ship.ship(item)]

print(CheckoutFacade().place_order("book", 250))
Output
legacy paid 4950 paise
add(2, 3) -> 5
['reserved book', 'charged 250', 'shipped book']

Strategy, Observer and Command

Python
# Strategy: pass the algorithm as a function
def flat_discount(price): return price - 100
def percent_discount(price): return price * 0.9
def no_discount(price): return price

def final_price(price, strategy=no_discount):
    return strategy(price)

print([final_price(1000, s) for s in (no_discount, flat_discount, percent_discount)])

# Observer: subscribers are notified of events
class EventBus:
    def __init__(self):
        self.subscribers = {}
    def subscribe(self, event, handler):
        self.subscribers.setdefault(event, []).append(handler)
    def publish(self, event, data):
        for handler in self.subscribers.get(event, []):
            handler(data)

bus = EventBus()
bus.subscribe("order_placed", lambda o: print("email sent for", o))
bus.subscribe("order_placed", lambda o: print("stock updated for", o))
bus.publish("order_placed", "WN-101")

# Command: actions as objects that can be undone
class AddText:
    def __init__(self, doc, text):
        self.doc, self.text = doc, text
    def execute(self):
        self.doc.append(self.text)
    def undo(self):
        self.doc.pop()

doc, history = [], []
for word in ["Hello", "Python", "World"]:
    cmd = AddText(doc, word)
    cmd.execute()
    history.append(cmd)
history.pop().undo()
print(doc)
Output
[1000, 900, 900.0]
email sent for WN-101
stock updated for WN-101
['Hello', 'Python']

Common Mistakes

  • Porting Java-style patterns literally (interfaces, getInstance()) where a function or module suffices.
  • Adding patterns before there is a real need, making simple code complex.
  • Implementing Singleton with global mutable state that makes testing hard.
  • Confusing the Decorator pattern with Python decorators — related ideas, different scopes.

Key Points to Remember

  • Creational: Singleton (module/cached function), Factory (dict of classes), Builder (chaining).
  • Structural: Adapter, Decorator (@ syntax), Facade.
  • Behavioural: Strategy (pass functions), Observer (event bus), Command (undoable actions).
  • Python's first-class functions and duck typing simplify many patterns.
  • Use patterns to solve real problems, not by default.

Practice the examples

Change an input, predict the result, then compare it with the output. Explain why the result changes.