โ† Back to Blog

23 GoF Patterns in Python

Jun 3, 2026 ยท 14 min read pythonpatternsjavacsharptutorial

The Gang of Four patterns are solutions to recurring design problems. Some languages need elaborate class hierarchies to implement them. Python doesn't โ€” first-class functions, duck typing, @decorators, __dunder__ methods, generators, and dataclasses absorb most of them into one-liners.

Here are all 23 patterns: what problem they solve, and the Pythonic implementation.

Just want the cheat sheet? Skip to the scorecard.


Creational Patterns

1. Singleton

What: Ensure a class has only one instance, accessible globally. Think database connections, config objects, or logger instances โ€” you don't want multiple copies floating around.

The simplest Python approach is a module-level variable:

# db.py
_connection = None

def get_db():
    global _connection
    if _connection is None:
        _connection = connect_db()
    return _connection

Modules are singletons by default in Python. Import it anywhere, get the same object. Note: this isn't thread-safe โ€” with multi-threaded servers, use threading.Lock:

import threading

_lock = threading.Lock()
_connection = None

def get_db():
    global _connection
    if _connection is None:
        with _lock:
            if _connection is None:  # double-check after acquiring lock
                _connection = connect_db()
    return _connection

If you need the class approach:

class DB:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance

Module-level variable for simplicity, __new__ override when you need a class.

2. Factory Method

What: Delegate object creation to a function so callers don't need to know the concrete class. Useful when the type to create depends on runtime config or input.

In Python, a function that returns an object:

class S3Store:
    def save(self, data: bytes):
        upload_to_s3(data)

class DiskStore:
    def save(self, data: bytes):
        Path("/tmp/data").write_bytes(data)

def new_store(kind: str):
    stores = {"s3": S3Store, "disk": DiskStore}
    return stores.get(kind, DiskStore)()

S3Store and DiskStore just need a save() method. No base class required โ€” duck typing handles the rest. If it has save(), it's a store.

3. Abstract Factory

What: Create families of related objects that must work together. A database kit needs a connection, transaction, and query builder that all speak the same dialect โ€” you don't want a Postgres connection paired with a MySQL query builder.

In Python, a dict of callables or a simple class:

def postgres():
    return {
        "connect": pg_connect,
        "new_tx": pg_begin_tx,
        "new_query": PgQueryBuilder,
    }

def mysql():
    return {
        "connect": mysql_connect,
        "new_tx": mysql_begin_tx,
        "new_query": MySQLQueryBuilder,
    }

# Usage
kit = postgres()
conn = kit["connect"](dsn)

Swap postgres() for mysql() and the whole family travels together.

For typed codebases where IDE support matters, use a Protocol:

from typing import Protocol

class DBKit(Protocol):
    def connect(self, dsn: str): ...  # ... means "no body" (same as pass)
    def new_tx(self, conn): ...

# No "implements" keyword โ€” just match the methods
class PostgresKit:
    def connect(self, dsn: str):
        return pg_connect(dsn)

    def new_tx(self, conn):
        return pg_begin_tx(conn)

def init_app(kit: DBKit, dsn: str):  # accepts ANY kit matching the protocol
    conn = kit.connect(dsn)

# Swap the kit, same code works
init_app(PostgresKit(), "postgres://localhost/mydb")
init_app(MySQLKit(), "mysql://localhost/mydb")

PostgresKit never references DBKit. It just has the right methods โ€” structural typing, like Go interfaces. Your IDE and mypy catch mismatches at dev time.

But for most Python code, the dict approach is simpler and sufficient.

4. Builder

What: Construct complex objects step by step, especially when an object has many optional parameters. Avoids constructors with 10 arguments where you can't tell which is which.

For simple config objects, Python has keyword arguments and dataclasses:

from dataclasses import dataclass

@dataclass
class Server:
    # These look like class fields but @dataclass turns them into instance fields via __init__
    port: int = 8080
    timeout: int = 30
    tls: bool = False
    host: str = "localhost"

# Only override what you care about
srv = Server(port=9090, tls=True)

Defaults in the class, keyword arguments at the call site. Python solved this at the language level.

For complex objects that need step-by-step construction with validation (e.g., building a SQL query), a fluent builder still makes sense:

class QueryBuilder:
    def __init__(self):
        self._select = []
        self._table = ""
        self._where = []

    def select(self, *cols):
        self._select.extend(cols)
        return self  # return self enables chaining

    def from_table(self, table):
        self._table = table
        return self

    def where(self, condition):
        self._where.append(condition)
        return self

    def build(self):
        sql = f"SELECT {', '.join(self._select)} FROM {self._table}"
        if self._where:
            sql += f" WHERE {' AND '.join(self._where)}"
        return sql

query = QueryBuilder().select("name", "age").from_table("users").where("age > 18").build()
# โ†’ "SELECT name, age FROM users WHERE age > 18"

But for config/options objects, @dataclass + kwargs is all you need.

5. Prototype

What: Create new objects by cloning an existing one instead of building from scratch. Useful for templates โ€” start with a base config, clone it, tweak per environment.

Python has copy:

import copy

base = {"host": "localhost", "port": 5432, "tags": ["prod"]}
staging = copy.deepcopy(base)
staging["host"] = "staging-db"  # doesn't affect base

For dataclasses:

from dataclasses import replace

@dataclass
class Config:
    host: str = "localhost"
    port: int = 5432

staging = replace(base_config, host="staging-db")

copy.deepcopy() for complex objects. dataclasses.replace() for shallow tweaks. One line.


Structural Patterns

6. Adapter

What: Make two incompatible interfaces work together. You have a legacy class with write_log() but your code expects log(). The adapter bridges the gap.

In Python, duck typing means you just implement the method:

class LogAdapter:
    def __init__(self, legacy):
        self.legacy = legacy

    def log(self, msg):
        self.legacy.write_log(msg)

Or even simpler โ€” if you own the class, just add the method:

LegacyLogger.log = LegacyLogger.write_log

If it has a log() method, it's a logger. No interface declarations needed โ€” duck typing handles compatibility.

# Your new code only knows about .log()
def process_request(logger):
    logger.log("request started")
    # ...
    logger.log("request done")

# New logger works directly
process_request(NewLogger())

# Old logger doesn't have .log() โ€” adapter bridges it
process_request(LogAdapter(old_legacy_logger))

Without the adapter, you'd rewrite every function that calls .log() to also handle .write_log(). The adapter lets you plug the old object into the new system without modifying either side.

7. Bridge

What: Separate an abstraction from its implementation so both can vary independently. Prevents combinatorial explosion โ€” without it, urgent+email, urgent+SMS, normal+email, normal+SMS each need their own class.

In Python, pass an object with the right protocol:

class Notification:
    def __init__(self, sender, urgent=False):
        self.sender = sender
        self.urgent = urgent

    def notify(self, to, msg):
        if self.urgent:
            msg = f"๐Ÿšจ URGENT: {msg}"
        self.sender.send(to, msg)

# Mix and match
Notification(EmailSender(), urgent=True).notify("[email protected]", "Server down")
Notification(SlackSender()).notify("#ops", "Deploy done")

sender just needs a send() method. Any object works. Duck typing is the bridge.

8. Composite

What: Treat individual objects and groups of objects uniformly. A file has a size. A directory has a size (sum of its contents). Your code shouldn't care which one it's dealing with.

In Python, same interface on both:

class File:
    def __init__(self, size):
        self.size = size

class Dir:
    def __init__(self, children):
        self.children = children

    @property
    def size(self):
        return sum(c.size for c in self.children)

A Dir contains items. Each item has .size. Could be a File or another Dir. Recursion happens naturally. No abstract base class needed.

9. Decorator

What: Add behavior to an object or function without modifying its source code. Wrap it with extra functionality (logging, timing, retry) that can be stacked.

Python has @decorator built into the language:

import functools

def with_logging(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        print(f"Calling {fn.__name__}")
        return fn(*args, **kwargs)
    return wrapper

@with_logging
def process_order(order_id):
    ...

@functools.wraps(fn) copies the original function's __name__, __doc__, and __module__ onto the wrapper. Without it, process_order.__name__ would be "wrapper" instead of "process_order" โ€” breaking stack traces, logging, and introspection tools. Always use it in decorators.

Stack them: @with_logging @with_timing @with_retry. The language has first-class support for this pattern.

10. Facade

What: Provide a simple interface to a complex subsystem. Hide internal complexity behind one clean function or module that does the orchestration.

In Python, a module with a clean public API:

# order.py
from ._inventory import reserve
from ._payment import charge
from ._shipping import schedule

def place_order(item, card):
    reserve(item)
    charge(card)
    schedule(item)

Consumers import place_order. They never see the internal modules. Every well-designed Python package is already a facade โ€” __init__.py controls what's public.

11. Flyweight

What: Share instances to save memory when many objects have identical state. Instead of 10,000 font objects (most identical), cache and reuse them.

Python has __new__ with a cache, or just lru_cache:

from functools import lru_cache

@lru_cache(maxsize=None)
def get_font(family, size, bold):
    return Font(family, size, bold)

# 10,000 characters, only a handful of Font allocations
chars = [Char("A", get_font("Arial", 12, False)) for _ in range(10000)]

lru_cache memoizes the constructor. Same arguments โ†’ same object. One decorator, pattern done.

12. Proxy

What: Control access to an object by wrapping it with a stand-in. The proxy adds behavior (caching, access control, lazy loading) without the caller knowing.

Python has __getattr__ delegation:

class CachedDB:
    def __init__(self, real_db):
        self._real = real_db
        self._cache = {}

    def query(self, sql):
        if sql not in self._cache:
            self._cache[sql] = self._real.query(sql)
        return self._cache[sql]

    def __getattr__(self, name):
        return getattr(self._real, name)

__getattr__ forwards everything you don't explicitly override. The caller doesn't know it's talking to a proxy. You only write code for the methods you intercept.


Behavioral Patterns

13. Strategy

What: Define a family of algorithms and make them interchangeable at runtime. The calling code doesn't change โ€” only the algorithm plugged in.

In Python you pass a function:

def full_price(base):
    return base

def half_off(base):
    return base * 0.5

def checkout(items, pricer):
    return sum(pricer(item.base) for item in items)

# Usage
checkout(cart, full_price)
checkout(cart, half_off)
checkout(cart, lambda b: b * 0.8)  # inline 20% off

The strategy is just a callable. Named function, lambda, method โ€” anything that takes a number and returns a number.

14. Observer

What: When one object changes state, notify all dependents automatically. Decouples the thing that changes from the things that react to it.

In Python, a list of callbacks:

class EventBus:
    def __init__(self):
        self._subs = {}

    def on(self, event, fn):
        self._subs.setdefault(event, []).append(fn)

    def emit(self, event, data):
        for fn in self._subs.get(event, []):
            fn(data)

bus = EventBus()
bus.on("user.created", send_welcome_email)
bus.on("user.created", track_signup)
bus.emit("user.created", user)

Subscribe with any callable. Emit fires them all.

15. Command

What: Encapsulate an action as an object so you can queue, undo, replay, or log operations. Each command knows how to execute and how to reverse itself.

In Python, functions in a list:

def execute(commands):
    done = []
    for cmd in commands:
        try:
            cmd["do"]()
            done.append(cmd)
        except Exception:
            for c in reversed(done):
                c["undo"]()
            raise

steps = [
    {"do": validate_config, "undo": lambda: None},
    {"do": build_image, "undo": remove_image},
    {"do": push_to_registry, "undo": delete_from_registry},
]
execute(steps)

Commands are data when they're functions. Queue them, replay them, reverse them.

16. Template Method

What: Define the skeleton of an algorithm, letting subclasses (or callers) override specific steps without changing the overall structure.

In Python, pass functions as the customizable steps:

def run_pipeline(data, *, validate, transform, store):
    validate(data)
    result = transform(data)
    store(result)

# CSV pipeline
run_pipeline(data, validate=validate_csv, transform=csv_to_json, store=write_to_s3)

# XML pipeline โ€” same skeleton, different steps
run_pipeline(data, validate=validate_xml, transform=xml_to_json, store=write_to_db)

Same flow, different behavior. Keyword arguments make the customizable steps explicit and readable.

17. Iterator

What: Traverse a collection without exposing its internal structure. The consumer just says "give me the next item" without knowing if it's a list, tree, or database cursor.

Python has __iter__ and __next__, but even better โ€” generators:

def fibonacci():
    a, b = 0, 1
    while True:
        yield a
        a, b = b, a + b

# Lazy, infinite, memory-efficient
for n in fibonacci():
    if n > 100:
        break
    print(n)

Any function with yield is an iterator. Lazy evaluation, no state management, no hasNext(). Works with for, list(), next(), and every other iteration tool.

18. State

What: An object's behavior changes based on its internal state. Instead of giant if/else chains, each state encapsulates its own behavior and transitions.

In Python, a function that returns the next function:

def idle(event):
    if event == "connect":
        return connected
    return idle

def connected(event):
    if event == "send":
        return connected
    if event == "disconnect":
        return idle
    return connected

# Run it
state = idle
state = state("connect")     # โ†’ connected
state = state("send")        # โ†’ connected
state = state("disconnect")  # โ†’ idle

Each state is a function that knows its transitions. The current state is just a variable holding the active function.

Tradeoff: Function-based state machines are concise but harder to debug โ€” stack traces show <function connected at 0x...> instead of a meaningful class name. For complex state machines in production, consider an enum or class-per-state approach for better observability.

19. Chain of Responsibility

What: Pass a request through a chain of handlers. Each handler decides whether to process it or pass it along. Think middleware: auth โ†’ rate limit โ†’ logging โ†’ handler.

In Python, decorators are middleware:

import functools

def with_auth(fn):
    @functools.wraps(fn)
    def wrapper(request):
        if not request.get("authenticated"):
            return {"status": 401, "body": "unauthorized"}
        return fn(request)
    return wrapper

def with_logging(fn):
    @functools.wraps(fn)
    def wrapper(request):
        print(f"Request: {request['path']}")
        return fn(request)
    return wrapper

@with_auth
@with_logging
def handle_order(request):
    return {"status": 200, "body": "ok"}

Stack decorators top to bottom. Each one decides whether to call the next. If you've written Python middleware, you've implemented Chain of Responsibility.

Note: Decorator execution order is bottom-to-top (innermost first). @with_auth wraps @with_logging which wraps handle_order. So auth checks happen first, then logging, then the handler.

20. Mediator

What: Centralize communication between objects so they don't reference each other directly. Components talk to the mediator; the mediator routes messages.

In Python, a central object with callbacks:

class ChatRoom:
    def __init__(self):
        self.users = {}

    def join(self, name, callback):
        self.users[name] = callback

    def send(self, sender, to, msg):
        if to in self.users:
            self.users[to](f"[{sender}]: {msg}")

room = ChatRoom()
room.join("alice", print)
room.join("bob", print)
room.send("alice", "bob", "hello")  # bob sees "[alice]: hello"

Users know the room. The room knows the users. Users don't know each other. That's the whole pattern.

21. Memento

What: Capture an object's state so you can restore it later (undo). Save snapshots without exposing the object's internal structure.

In Python, copy or dataclass snapshot:

import copy

class Editor:
    def __init__(self):
        self.content = ""
        self._history = []

    def snapshot(self):
        self._history.append(copy.copy(self.content))

    def undo(self):
        if self._history:
            self.content = self._history.pop()

A list is your undo stack. copy.copy() captures the state.

22. Visitor

You have an AST (Abstract Syntax Tree โ€” how parsed expressions like 2 + 3 * 4 are represented as a tree of nodes). You want to add operations (evaluate, print) without modifying the node classes.

Python has functools.singledispatch:

from dataclasses import dataclass
from functools import singledispatch

@dataclass
class Number:
    value: float

@dataclass
class Add:
    left: object
    right: object

@dataclass
class Mul:
    left: object
    right: object

@singledispatch
def evaluate(node):
    raise TypeError(f"Unknown node: {type(node)}")

@evaluate.register
def _(node: Number):
    return node.value

@evaluate.register
def _(node: Add):
    return evaluate(node.left) + evaluate(node.right)

@evaluate.register
def _(node: Mul):
    return evaluate(node.left) * evaluate(node.right)

# evaluate(Add(Number(2), Mul(Number(3), Number(4)))) โ†’ 14

New operation? Write a new @singledispatch function. No changes to node classes. No accept() method. No double dispatch. The stdlib gives you type-based dispatch for free.

23. Interpreter

Users express rules in a mini-language. "Electronics over $50." You need to parse and execute.

In Python, composable functions with operator overloading:

class Filter:
    def __init__(self, fn):
        self.fn = fn

    def __call__(self, item):
        return self.fn(item)

    def __and__(self, other):
        return Filter(lambda i: self(i) and other(i))

    def __or__(self, other):
        return Filter(lambda i: self(i) or other(i))

expensive = Filter(lambda i: i.price > 50)
electronics = Filter(lambda i: i.category == "electronics")

query = expensive & electronics  # operator overloading!
results = [item for item in catalog if query(item)]

__and__ and __or__ let you compose filters with & and |. Each filter is a callable. Combine them with Python operators. No expression tree hierarchy, no parser classes.

Why &/| and not and/or? Python doesn't allow overloading and/or (logical operators). But &/| (bitwise operators) can be overloaded via __and__/__or__ โ€” so we repurpose them for composition. This is the same trick Django ORM, SQLAlchemy, and Pandas use.


The Scorecard

# Pattern What it solves Python
Creational
1 Singleton One global instance Module-level or __new__
2 Factory Method Create without specifying class Function returning object
3 Abstract Factory Families of related objects Dict of callables
4 Builder Complex object step by step @dataclass + kwargs
5 Prototype Clone instead of rebuild copy.deepcopy()
Structural
6 Adapter Make interfaces compatible Duck typing
7 Bridge Separate abstraction from impl Object with protocol
8 Composite Treat single and group the same List of same interface
9 Decorator Add behavior without modifying @decorator (built-in!)
10 Facade Simple API over complex system Module exports
11 Flyweight Share instances, save memory @lru_cache
12 Proxy Control access to an object __getattr__ delegation
Behavioral
13 Strategy Swap algorithms at runtime Pass a function
14 Observer Notify on state change List of callbacks
15 Command Queue, undo, replay actions Functions in a list
16 Template Method Fixed skeleton, custom steps Functions as kwargs
17 Iterator Traverse without exposing internals yield (generators)
18 State Behavior changes with state Function โ†’ next function
19 Chain of Responsibility Request through handler chain Stacked @decorators
20 Mediator Centralize communication Central object + callbacks
21 Memento Capture/restore state (undo) copy.copy()
22 Visitor Add ops without modifying classes @singledispatch
23 Interpreter Evaluate a mini-language __dunder__ overloading

What This Means

Python didn't remove design patterns. It absorbed them into the language.

First-class functions kill every pattern that exists to pass behavior around. Strategy, Command, Observer, Template Method โ€” just pass a function. Duck typing kills every pattern that exists to make types compatible. Adapter, Bridge โ€” if it has the right methods, it works. Decorators kill the Decorator pattern โ€” the language has @ syntax for it. Generators kill the Iterator pattern โ€” yield and you're done. Dunder methods kill Proxy, Flyweight, and Interpreter โ€” __getattr__, __new__, __and__ handle them naturally.

You don't need to memorize 23 patterns. You need to understand five Python features: functions as values, duck typing, decorators, generators, and dunder methods. The rest follows.

Not all patterns disappear completely. Composite is still a recursive tree worth naming. State machines are still state machines. Mediator still coordinates components. But the implementation drops from "three classes and an interface" to "five lines and a function."

If you're coming from Java or C# and wondering where all the patterns went โ€” they're still here. They're just not ceremonies anymore. They're Python.

Want to write Pythonic code that uses these patterns naturally? Check out DSA Concepts in Python on ByteLearn โ€” free, no signup wall.

Got thoughts on this post?

I'd love to hear from you. Reach out on any of these:

Want to learn by doing?

ByteLearn.dev has free courses with interactive quizzes for developers.

Browse courses โ†’
ยฉ 2026 ByteLearn.dev. Free courses for developers. ยท Privacy