Skip to main content
Python · lesson 32 of 34
Python advanced Lesson 32 of 34

Also part of: Low-Level Design

Python Design Patterns

Run singleton, factory, observer, strategy, decorator and command patterns in idiomatic Python, and see where functions and modules replace classes.

By SkillByExample · Updated

Design patterns are named solutions to recurring design problems. Python’s first-class functions and modules make several of them much smaller than their textbook Java versions. Output below is from Python 3.13.

Singleton

A class that always hands back the same instance — overriding __new__ is the classic way, and it has a trap.

class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, value):
        self.value = value

s1 = Singleton("first")
s2 = Singleton("second")
print(s1 is s2, s1.value)
$ python singleton.py
True second

__new__ returned the same object both times, but Python still calls __init__ on every construction, so the second call overwrote value. Guarding __init__ fixes that, but the more Pythonic singleton needs no class tricks at all:

# settings.py
class Settings:
    def __init__(self):
        self.debug = False

settings = Settings()        # created once, on first import
import settings
from settings import settings as s

s.debug = True
import settings as again     # a second import returns the cached module
print(again.settings.debug, again is settings)
$ python use_settings.py
True True

Python caches modules in sys.modules, so every import settings gets the same module object and therefore the same settings instance.

Factory

A factory chooses which class to build from data — in Python, a dict of classes is usually enough.

class Email:
    def __init__(self, address: str):
        self.address = address
    def send(self, msg: str) -> None:
        print(f"email to {self.address}: {msg}")

class SMS:
    def __init__(self, phone: str):
        self.phone = phone
    def send(self, msg: str) -> None:
        print(f"sms to {self.phone}: {msg}")

CHANNELS = {"email": Email, "sms": SMS}

def create_notifier(kind: str, **kwargs):
    try:
        return CHANNELS[kind](**kwargs)
    except KeyError:
        raise ValueError(f"unknown channel: {kind!r}") from None

for kind, args in [("email", {"address": "a@example.com"}), ("sms", {"phone": "555-0100"})]:
    create_notifier(kind, **args).send("order shipped")
try:
    create_notifier("fax")
except ValueError as e:
    print(e)
$ python factory.py
email to a@example.com: order shipped
sms to 555-0100: order shipped
unknown channel: 'fax'

The caller names a channel as a string — say, from a config file — and never imports Email or SMS. Adding a channel means one new class and one dict entry. from None hides the internal KeyError so the traceback shows only the meaningful error.

Observer

Subscribers register callbacks; the subject calls them all when an event fires.

from collections import defaultdict
from typing import Any, Callable

class EventEmitter:
    def __init__(self):
        self._listeners: defaultdict[str, list[Callable]] = defaultdict(list)

    def on(self, event: str, listener: Callable) -> None:
        self._listeners[event].append(listener)

    def off(self, event: str, listener: Callable) -> None:
        self._listeners[event].remove(listener)

    def emit(self, event: str, *args: Any) -> None:
        for listener in list(self._listeners[event]):
            listener(*args)

def confirm(order): print(f"confirmation for order {order['id']}")
def charge(order): print(f"charging ${order['total']:.2f}")

bus = EventEmitter()
bus.on("order_placed", confirm)
bus.on("order_placed", charge)
bus.emit("order_placed", {"id": 42, "total": 99.99})
bus.off("order_placed", charge)
bus.emit("order_placed", {"id": 43, "total": 5})
bus.emit("nobody_listens")
$ python observer.py
confirmation for order 42
charging $99.99
confirmation for order 43

Listeners run in registration order. After off, order 43 only got a confirmation, and an event with no listeners is a silent no-op. emit iterates over a copy of the list so a listener can unsubscribe itself mid-emit without skipping its neighbour.

Strategy

A strategy is an interchangeable algorithm. In Python, pass a function.

def by_price(item): return item["price"]
def by_name(item): return item["name"]
def by_value(item): return item["rating"] / item["price"]

products = [
    {"name": "kettle", "price": 30, "rating": 4.5},
    {"name": "blender", "price": 80, "rating": 4.8},
    {"name": "airfryer", "price": 60, "rating": 4.9},
]

def show(strategy, reverse=False):
    ordered = sorted(products, key=strategy, reverse=reverse)
    print(f"{strategy.__name__:9}", [p["name"] for p in ordered])

show(by_price)
show(by_name)
show(by_value, reverse=True)
$ python strategy.py
by_price  ['kettle', 'airfryer', 'blender']
by_name   ['airfryer', 'blender', 'kettle']
by_value  ['kettle', 'airfryer', 'blender']

sorted’s key= is the strategy pattern: the sorting algorithm stays fixed and the comparison rule is swapped in. Note that the cheap kettle wins on rating-per-dollar even though it has the lowest rating. A class per strategy would only be worth it if strategies carried their own state.

Decorator (the object pattern)

Wrappers that share an interface can be stacked at runtime, each adding one behaviour.

import base64
import zlib

class MemorySource:
    def __init__(self): self.stored = ""
    def write(self, data: str) -> None: self.stored = data
    def read(self) -> str: return self.stored

class Reversing:                         # toy "encryption"
    def __init__(self, inner): self.inner = inner
    def write(self, data: str) -> None: self.inner.write(data[::-1])
    def read(self) -> str: return self.inner.read()[::-1]

class Compressing:
    def __init__(self, inner): self.inner = inner
    def write(self, data: str) -> None:
        self.inner.write(base64.b64encode(zlib.compress(data.encode())).decode())
    def read(self) -> str:
        return zlib.decompress(base64.b64decode(self.inner.read())).decode()

raw = MemorySource()
source = Compressing(Reversing(raw))
source.write("hello hello hello hello")
print(raw.stored)
print(source.read())
$ python wrappers.py
==QsIMAaBcCQIfVyJ3MSLzJe
hello hello hello hello

The caller sees only write and read. Underneath, the text was compressed, base64-encoded, then reversed — which is why the stored string starts with ==, base64’s padding. Reading peels the layers off in reverse.

Command

Each operation becomes an object with execute and undo; a history list gives you undo for free.

class Editor:
    def __init__(self):
        self.text = ""
        self.history: list = []

    def run(self, cmd) -> None:
        cmd.execute(self)
        self.history.append(cmd)

    def undo(self) -> None:
        if self.history:
            self.history.pop().undo(self)

class Insert:
    def __init__(self, s: str): self.s = s
    def execute(self, ed: Editor) -> None: ed.text += self.s
    def undo(self, ed: Editor) -> None: ed.text = ed.text[: -len(self.s)]

class Upper:
    def execute(self, ed: Editor) -> None:
        self.before = ed.text
        ed.text = ed.text.upper()
    def undo(self, ed: Editor) -> None: ed.text = self.before

ed = Editor()
for cmd in [Insert("hello"), Insert(", world"), Upper()]:
    ed.run(cmd)
    print(repr(ed.text))
ed.undo(); print("undo:", repr(ed.text))
ed.undo(); print("undo:", repr(ed.text))
$ python command.py
'hello'
'hello, world'
'HELLO, WORLD'
undo: 'hello, world'
undo: 'hello'

Insert can undo by trimming, but Upper can’t be inverted (it loses the original case), so it saves the previous text when it executes. Each command stores exactly what it needs to reverse itself.

Practice

1. Fix singleton.py so the second construction does not overwrite value, and print s1.value again.

Solution
class Singleton:
    _instance = None

    def __new__(cls, *args, **kwargs):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

    def __init__(self, value):
        if not hasattr(self, "value"):
            self.value = value

s1 = Singleton("first")
s2 = Singleton("second")
print(s1 is s2, s1.value)
$ python singleton_fixed.py
True first

__init__ still runs twice; the guard just makes the second run do nothing.

2. Add a "push" channel to the factory that takes no arguments, without changing create_notifier.

Solution
class SMS:
    def __init__(self, phone: str):
        self.phone = phone
    def send(self, msg: str) -> None:
        print(f"sms to {self.phone}: {msg}")

CHANNELS = {"sms": SMS}

def create_notifier(kind: str, **kwargs):
    try:
        return CHANNELS[kind](**kwargs)
    except KeyError:
        raise ValueError(f"unknown channel: {kind!r}") from None

# the extension: a new class and one registry entry
class Push:
    def send(self, msg: str) -> None:
        print(f"push: {msg}")

CHANNELS["push"] = Push

create_notifier("push").send("order shipped")
create_notifier("sms", phone="555-0100").send("order shipped")
$ python factory_push.py
push: order shipped
sms to 555-0100: order shipped

The factory is open for extension through the registry — only the dict changed.

3. Add a once(event, listener) method to EventEmitter that removes the listener after its first call.

Solution
from collections import defaultdict

class EventEmitter:
    def __init__(self):
        self._listeners = defaultdict(list)

    def on(self, event, listener): self._listeners[event].append(listener)
    def off(self, event, listener): self._listeners[event].remove(listener)

    def once(self, event, listener):
        def wrapper(*args):
            self.off(event, wrapper)
            listener(*args)
        self.on(event, wrapper)

    def emit(self, event, *args):
        for listener in list(self._listeners[event]):
            listener(*args)

bus = EventEmitter()
bus.once("ready", lambda: print("first ready only"))
bus.on("ready", lambda: print("every ready"))
bus.emit("ready")
bus.emit("ready")
$ python once.py
first ready only
every ready
every ready

The wrapper unsubscribes itself during emit — which only works because emit loops over a copy of the list.

4. Add redo to the command example: undone commands go onto a redo stack, and redo() re-executes the most recent one.

Solution
class Editor:
    def __init__(self):
        self.text, self.history, self.redo_stack = "", [], []

    def run(self, cmd):
        cmd.execute(self)
        self.history.append(cmd)
        self.redo_stack.clear()

    def undo(self):
        cmd = self.history.pop()
        cmd.undo(self)
        self.redo_stack.append(cmd)

    def redo(self):
        cmd = self.redo_stack.pop()
        cmd.execute(self)
        self.history.append(cmd)

class Insert:
    def __init__(self, s): self.s = s
    def execute(self, ed): ed.text += self.s
    def undo(self, ed): ed.text = ed.text[: -len(self.s)]

ed = Editor()
ed.run(Insert("a")); ed.run(Insert("b"))
ed.undo(); print(ed.text)
ed.redo(); print(ed.text)
$ python redo.py
a
ab

Running a new command clears the redo stack, as every editor does — redoing after a fresh edit would replay history that no longer applies.

Frequently Asked Questions

Are design patterns necessary in Python?
Many Gang of Four patterns shrink in Python because functions are first-class objects, modules are singletons and duck typing removes the need for interface hierarchies. Learn each pattern as the solution to a specific problem, and reach for the simplest Python form that solves it.
Is the Singleton pattern bad?
A singleton is global state with extra steps: it couples every caller to one instance and leaks state between tests. Prefer creating one object at start-up and passing it to the code that needs it, or a module-level instance when global really is right.
What's the difference between the decorator pattern and Python decorators?
The decorator design pattern wraps an object in another object with the same interface to add behaviour, and wrappers can be stacked at runtime. Python's @decorator syntax wraps functions or classes at definition time; it can implement the pattern but is a more general language feature.
How do I implement the strategy pattern in Python?
Usually by passing a function. A strategy is just an interchangeable piece of behaviour, so a callable parameter such as key= in sorted() does the job without defining a class per strategy. Use classes only when a strategy needs its own configuration or state.
What is the command pattern used for?
It turns each operation into an object that knows how to execute and undo itself. Keeping those objects in a history list gives you undo and redo, and the same objects can be queued, logged or replayed later.