class DataViewer:
def __init__(self, filename: str):
self.connection = ? # How to create the right connection?
def print_tables(self):
tables = self.connection.list_tables()
print(tables)Design Patterns I
Lessons from the Gang of Four
Introduction: What Are Design Patterns?
In the 1990s, four authors published a book that would fundamentally shape how programmers think about software design. They became known as the “Gang of Four”—not to be confused with the Maoist faction of the same name.

Design patterns are reusable solutions to common problems in software design. They’re like recipes: not the final dish, but a proven template for creating something that works.
The Gang of Four (GoF) cataloged 23 patterns, organizing them into three categories:
- Creational: How objects are created
- Structural: How objects are composed
- Behavioral: How objects interact and communicate
These patterns emerged from Smalltalk and C++ development, languages quite different from Python. Which brings us to an important point…
Not all GoF patterns are useful in Python! Some patterns solve problems that don’t exist in Python, while others are already built into the language itself.
Blindly copying Java or C++ patterns to Python often creates unnecessarily complex code.
The Python Perspective
Python developer Brandon Rhodes gave an excellent talk categorizing GoF patterns by their usefulness in Python:
6 patterns rendered useless because Python has first-class functions: - Factory Method, Abstract Factory, Prototype, Singleton, Template Method, Strategy
2 patterns built into the language: - Iterator (generators and the iterator protocol) - Visitor (AST node visitors, but rarely needed)
3 patterns to avoid (hide data, scatter logic): - Proxy, Memento, Observer
4 “behaviors” (reasonable but rarely used): - Bridge, Decorator, Mediator, State
3 “keepers” (small but useful): - Builder (e.g., matplotlib’s fluent interface) - Adapter (e.g., socket.makefile()) - Flyweight (e.g., small integer caching)
4 “big ones” (important patterns): - Composite, Chain of Responsibility, Command, Interpreter
1 “odd one”: - Facade
This lecture focuses on a few key patterns: showing why some are obsolete in Python and diving deep into the ones that matter.
Pattern #1: The Factory Method Pattern (Mostly Obsolete)
Let’s start with a pattern you don’t need in modern Python.
The Problem
You’re building a database viewer that needs to work with different database backends:
How do you handle multiple database types (SQLite, DuckDB, PostgreSQL) without hardcoding the choice?
The GoF Solution: Factory Method
The traditional approach creates a factory method that subclasses override:
class Connection:
def list_tables(self) -> list[str]:
return []
class DataViewer:
def __init__(self, filename: str):
self.connection = self.make_connection(filename)
def make_connection(self, filename: str) -> Connection:
# Subclasses override this "factory method"
return Connection(filename)
def print_tables(self):
self.connection.list_tables()
class SqliteDataViewer(DataViewer):
def make_connection(self, filename: str):
return SqliteConnection(filename)
class DuckDBDataViewer(DataViewer):
def make_connection(self, filename: str):
return DuckDBConnection(filename)This works, but requires creating a new DataViewer subclass for every connection type. Tedious!
The Python Way: Pass the Class
Python has first-class functions and classes. Just pass the class directly:
class Connection:
def __init__(self, filename: str):
pass
def list_tables(self) -> list[str]:
return []
class DataViewer:
def __init__(self, filename: str, connection_type: type[Connection]):
self.connection = connection_type(filename)
def print_tables(self):
self.connection.list_tables()
# Usage - just pass the class itself!
viewer = DataViewer("mydb.db", SqliteConnection)No factory method, no subclasses, no boilerplate.
Alternative: Pass a Factory Function
You can also pass any callable that produces a connection:
from typing import Callable
class DataViewer:
def __init__(self, filename: str, factory: Callable[[str], Connection]):
self.connection = factory(filename)
# Pass the class (it's callable)
viewer1 = DataViewer("mydb.db", SqliteConnection)
# Or pass a custom function
def make_sqlite_connection(filename: str) -> Connection:
filename = filename.strip()
return SqliteConnection(filename)
viewer2 = DataViewer("mydb.db", make_sqlite_connection)In Python, you rarely need the traditional Factory Method pattern. Instead:
- Simple case: Pass the class or constructor function directly
- Complex case: Use a configuration dictionary or registry
- Very complex case: Consider the Builder pattern (see later section)
The factory pattern shines in statically-typed languages where you can’t easily pass classes around. Python doesn’t have that limitation.
Pattern #2: The Iterator Pattern (Built-In)
The Iterator pattern is another example of GoF thinking that Python has internalized.
The Problem: Complex Iteration Logic
Imagine you have nested data about passengers:
passengers = {
"People": [
{"name": "Frank", "age": 10, "weight": 20},
{"name": "Pete", "age": 5, "weight": 15}
],
"Animals": {
"Dogs": [{"name": "Blackie", "age": 2, "weight": 15}],
"Cats": [{"name": "Miauw", "age": 7, "weight": 3}]
}
}You want to calculate total weight. The naive approach mixes iteration logic with business logic:
total_weight = 0
for person in passengers["People"]:
total_weight += person["weight"]
for animal_type in passengers["Animals"].values():
for animal in animal_type:
total_weight += animal["weight"]
print(f"Total weight: {total_weight}")Total weight: 53
Can we separate the “how to iterate” from the “what to do with each item”?
The GoF Solution: Iterator Classes
Traditionally, you’d create iterator classes with __iter__ and __next__ methods. We covered this in the Iterators, Generators, and Decorators lecture.
The Python Way: Generators
Just use a generator function:
def all_passengers(passengers):
"""Iterate over all passengers regardless of type"""
for person in passengers["People"]:
yield person
for animal_type in passengers["Animals"].values():
for animal in animal_type:
yield animal
# Now the iteration logic is separate
total_weight = sum(p["weight"] for p in all_passengers(passengers))
print(f"Total weight: {total_weight}")Total weight: 53
You can create different iterators for different needs:
def dogs_only(passengers):
"""Iterate over dogs only"""
for dog in passengers["Animals"]["Dogs"]:
yield dog
for dog in dogs_only(passengers):
print(f"{dog['name']} is ready!")Blackie is ready!
If this feels familiar, it’s because you already studied the Iterator pattern—you just called it “generators”! Python has made the pattern so natural that you use it without thinking about the formal pattern name.
The GoF Iterator pattern requires multiple classes and boilerplate. Python’s generators give you the same power with simple functions and yield.
Pattern #3: The Composite Pattern ⭐
Finally, a pattern that’s genuinely useful in Python! The Composite pattern is one of the “big four” structural patterns.
The Core Idea
The Composite pattern lets you build tree structures where:
- Individual objects (leaves) and
- Containers of objects (branches)
…share the same interface.
Real-world examples:
- File systems: Folders contain folders contain files
- GUI hierarchies: Windows contain panels contain buttons
- Organization charts: Departments contain teams contain employees
- Document structures: Sections contain paragraphs contain text
The key insight: If leaves and branches implement the same methods, you can treat them uniformly. You don’t need to check “is this a container or a leaf?”
Example 1: Generic Tree Structure
Let’s build a tree that can hold numeric values:
class TreeNode:
"""A leaf node - holds a single value"""
def __init__(self, value):
self.value = value
def __repr__(self):
return f'TreeNode({self.value})'
def get_value(self):
return self.value
def set_value(self, value):
self.value = value
def total(self):
return self.value
class TreeValue:
"""A branch node - holds multiple children"""
def __init__(self, **kwargs):
self.children = kwargs
def __repr__(self):
items = ', '.join(f'{key}={child!r}' for key, child in self.children.items())
return f'TreeValue({items})'
def get_value(self):
return {
key: child.get_value()
for key, child in self.children.items()
}
def set_value(self, value_dict):
for key, value in value_dict.items():
self.children[key].set_value(value)
def total(self):
return sum(child.total() for child in self.children.values())Now we can build nested structures:
mydata = TreeValue(
first_tab=TreeValue(
width=TreeNode(20),
length=TreeNode(10),
),
second_tab=TreeValue(
upper=TreeValue(
width=TreeNode(20),
length=TreeNode(5)
),
lower=TreeValue(
x=TreeNode(4),
y=TreeNode(3)
)
)
)
print("Structure:", mydata)
print("\nValues:", mydata.get_value())
print("\nTotal:", mydata.total())Structure: TreeValue(first_tab=TreeValue(width=TreeNode(20), length=TreeNode(10)), second_tab=TreeValue(upper=TreeValue(width=TreeNode(20), length=TreeNode(5)), lower=TreeValue(x=TreeNode(4), y=TreeNode(3))))
Values: {'first_tab': {'width': 20, 'length': 10}, 'second_tab': {'upper': {'width': 20, 'length': 5}, 'lower': {'x': 4, 'y': 3}}}
Total: 62
Notice how TreeValue and TreeNode both implement:
get_value()- returns dataset_value()- updates data
total()- computes sum
We can update nested values:
data = {
'first_tab': {'width': 25, 'length': 15},
'second_tab': {
'upper': {'width': 30, 'length': 8},
'lower': {'x': 6, 'y': 12}
}
}
mydata.set_value(data)
print("New total:", mydata.total())New total: 96
Example 2: TKinter Application State
Here’s a practical application: managing form state in a GUI. We’ll evolve the solution through three stages.
Stage 1: Hardcoded Variables
First attempt—hardcode each variable:
import tkinter as tk
import tkinter.ttk as ttk
class AppState:
def __init__(self, val1, val2):
self.evar1 = tk.StringVar(value=val1)
self.evar2 = tk.StringVar(value=val2)
def get(self):
return {
'var1': self.evar1.get(),
'var2': self.evar2.get()
}
def set(self, value):
self.evar1.set(value['var1'])
self.evar2.set(value['var2'])
app_state = AppState("abc", "xyz")
# Create GUI elements
master = tk.Tk()
e1 = ttk.Entry(master, textvariable=app_state.evar1)
e2 = ttk.Entry(master, textvariable=app_state.evar2)
# ... etcThis works but doesn’t scale. Every new field requires code changes.
Stage 2: Dictionary-Based
Use a dictionary to make it flexible:
class AppState:
def __init__(self, string_vars: dict[str, tk.StringVar]):
self.string_vars = string_vars
def get(self):
return {
key: value.get()
for key, value in self.string_vars.items()
}
def set(self, value_dict: dict[str, str]):
for key, value in value_dict.items():
self.string_vars[key].set(value)
app_state = AppState({
'var1': tk.StringVar(value="abc"),
'var2': tk.StringVar(value="xyz"),
'var3': tk.StringVar(value="123"),
'var4': tk.StringVar(value="789"),
})
# Access by key
e1 = ttk.Entry(master, textvariable=app_state.string_vars['var1'])Better! But what if we have multiple tabs or sections?
Stage 3: Composite Pattern (The Solution)
Add __getitem__ to allow nesting:
class AppState:
def __init__(self, string_vars: dict[str, tk.StringVar]):
self.string_vars = string_vars
def get(self):
return {
key: value.get()
for key, value in self.string_vars.items()
}
def set(self, value_dict: dict[str, str]):
for key, value in value_dict.items():
self.string_vars[key].set(value)
def __getitem__(self, key):
return self.string_vars[key]
# Now we can nest AppState inside AppState!
app_state = AppState({
'tab1': AppState({
'var1': tk.StringVar(value="abc"),
'var2': tk.StringVar(value="xyz"),
}),
'tab2': AppState({
'var3': tk.StringVar(value="123"),
'var4': tk.StringVar(value="789"),
}),
'var5': tk.IntVar(value=3),
})
# Hierarchical access!
e1 = ttk.Entry(master, textvariable=app_state['tab1']['var1'])
e2 = ttk.Entry(master, textvariable=app_state['tab2']['var3'])Now we can organize state hierarchically: tabs contain sections contain fields. The same AppState class works at every level.
def swap_tab1():
state = app_state.get()
tab1 = state['tab1']
tab1['var1'], tab1['var2'] = tab1['var2'], tab1['var1']
app_state.set(state)The composite pattern shines when you have:
- Hierarchical data (trees, nested structures)
- Uniform interface (leaves and branches share methods)
- Recursive operations (traverse/process the whole tree)
Without the pattern, you’d need type checking everywhere:
if isinstance(item, Container):
for child in item.children:
process(child)
elif isinstance(item, Leaf):
process_leaf(item)With the pattern, just:
item.process() # Works for both!Pattern #4: The Builder Pattern
The Builder pattern separates object construction from representation. It’s useful when:
- Objects have many optional parameters
- Construction is complex or has multiple steps
- You want a fluent interface (method chaining)
The Problem: Complex Configuration
Imagine creating a plot with matplotlib. Without a builder, it’s messy:
import matplotlib.pyplot as plt
# Direct approach - lots of individual calls
fig = plt.figure(figsize=(10, 6))
ax = fig.add_subplot(111)
ax.plot([1, 2, 3], [1, 4, 9])
ax.set_xlabel('x values')
ax.set_ylabel('y values')
ax.set_title('My Plot')
ax.grid(True)
ax.legend(['Quadratic'])
plt.show()Or with nested dictionaries:
config = {
'figsize': (10, 6),
'xlabel': 'x values',
'ylabel': 'y values',
'title': 'My Plot',
'grid': True,
'legend': ['Quadratic']
}
# Still need to apply these somehow...The Builder Solution: Fluent Interface
Matplotlib actually uses a builder-like pattern:
import matplotlib.pyplot as plt
# Fluent interface - chain methods
(plt.figure(figsize=(10, 6))
.add_subplot(111)
.plot([1, 2, 3], [1, 4, 9])
.set(xlabel='x values',
ylabel='y values',
title='My Plot'))
plt.grid(True)
plt.legend(['Quadratic'])
plt.show()Let’s build our own simple builder:
class PlotBuilder:
"""A fluent interface for creating plots"""
def __init__(self):
self._data = []
self._labels = []
self._title = None
self._xlabel = None
self._ylabel = None
self._figsize = (8, 6)
def add_line(self, x, y, label=None):
"""Add a line to the plot"""
self._data.append((x, y))
self._labels.append(label)
return self # Return self for chaining!
def title(self, title):
"""Set plot title"""
self._title = title
return self
def xlabel(self, label):
"""Set x-axis label"""
self._xlabel = label
return self
def ylabel(self, label):
"""Set y-axis label"""
self._ylabel = label
return self
def figsize(self, width, height):
"""Set figure size"""
self._figsize = (width, height)
return self
def build(self):
"""Actually create the plot"""
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=self._figsize)
for (x, y), label in zip(self._data, self._labels):
ax.plot(x, y, label=label)
if self._title:
ax.set_title(self._title)
if self._xlabel:
ax.set_xlabel(self._xlabel)
if self._ylabel:
ax.set_ylabel(self._ylabel)
if any(self._labels):
ax.legend()
return fig, ax
def show(self):
"""Build and display the plot"""
self.build()
import matplotlib.pyplot as plt
plt.show()
# Usage - clean and readable
plot = (
PlotBuilder()
.title("Quadratic and Cubic Functions")
.xlabel("x")
.ylabel("f(x)")
.figsize(10, 6)
.add_line([1, 2, 3, 4], [1, 4, 9, 16], label="x²")
.add_line([1, 2, 3, 4], [1, 8, 27, 64], label="x³")
)
print("Plot configured successfully!")
# plot.show() # Uncomment to displayPlot configured successfully!
The key features:
- Fluent interface: Each method returns
self, allowing chaining - Flexible order: Call methods in any order
- Optional parameters: Only set what you need
- Clear intent: Code reads like configuration
- Separation: Building (configuration) is separate from creation
Compare the builder:
plot = (
PlotBuilder()
.title("My Plot")
.xlabel("x")
.add_line(x, y)
)To a mega-constructor:
plot = Plot(
title="My Plot",
xlabel="x",
ylabel=None,
lines=[(x, y)],
figsize=(8, 6),
grid=False,
# ... 20 more parameters
)The builder is more readable and flexible.
Real-World Builder Examples
Python libraries that use builder patterns:
SQLAlchemy (query building):
query = (select(User)
.where(User.age > 18)
.order_by(User.name)
.limit(10))Pandas (method chaining):
df = (pd.read_csv('data.csv')
.dropna()
.query('age > 18')
.groupby('city')
.mean())Pathlib (path construction):
path = Path.home() / 'documents' / 'project' / 'file.txt'The builder pattern is alive and well in Python—it’s just more implicit than in languages like Java or C#.
When to Use (and Not Use) Design Patterns
Design patterns are tools, not goals.
Use patterns when:
✅ Multiple developers need a shared vocabulary
✅ The pattern genuinely simplifies your code
✅ You’re working with a framework that expects certain patterns
✅ The problem is a natural fit (e.g., tree structures → Composite)
Don’t use patterns when:
❌ A simpler Python idiom exists (e.g., factories → pass the class)
❌ You’re pattern-matching just to pattern-match (“we must use a pattern!”)
❌ It makes code harder to read (over-engineering)
❌ The pattern solves a problem you don’t have
Patterns are like spices in cooking: a little enhances flavor, too much ruins the meal.
Bad pattern usage signals: - Your code has more abstraction layers than logic - Colleagues say “this is clever” (not “this is clear”) - You need a diagram to explain your simple task - You’re using patterns because they’re in your design patterns book
Good pattern usage signals: - The code is easier to understand than before - Adding new features requires minimal changes - The structure matches the problem domain naturally - Your colleagues say “oh, that makes sense!”
Key Takeaways
Many GoF patterns are obsolete in Python because Python has first-class functions, dynamic typing, and built-in features like generators.
The Composite pattern is genuinely useful when dealing with tree structures where leaves and branches should be treated uniformly.
The Builder pattern provides clean APIs for complex object construction, especially with fluent interfaces.
Patterns are means, not ends. Use them when they genuinely improve your code, not because they exist.
Think Pythonically first. Before reaching for a design pattern, ask: “Is there a simpler Python way?”
Exercises
Additional Resources
- Brandon Rhodes - Python Design Patterns - Excellent talk on GoF patterns in Python
- Python Design Patterns - Comprehensive guide to Pythonic patterns
- “Fluent Python” by Luciano Ramalho - Chapter 10 on design patterns
- Gang of Four Original Book - The classic reference (but remember: written for C++/Smalltalk!)
- Refactoring Guru - Design Patterns - Great visual explanations