Classical OOP design patterns (Gang of Four, 1994) come from Java/C++. In Python, many simplify dramatically; some become unnecessary. Knowing which apply matters.
Patterns that simplify in Python
Strategy
GoF: define a family of algorithms via interfaces.
Python: pass functions directly.
# Java-style
class SortStrategy:
def sort(self, items): ...
class QuickSort(SortStrategy): ...
class MergeSort(SortStrategy): ...
# Python: just functions
def sort_items(items, strategy):
return strategy(items)
sort_items(items, sorted)
sort_items(items, custom_sort)
Functions are first-class. No interface needed.
Decorator
GoF: wrap an object to extend behavior.
Python: literally the @decorator syntax.
@cache
@log_calls
def expensive():
...
Covered in the decorators lesson.
Iterator
GoF: traverse a collection without exposing structure.
Python: built-in iteration protocol.
for item in collection: # collection just needs __iter__
...
Every Python object can be iterable. __iter__ and __next__ are the protocol.
Singleton
GoF: ensure only one instance exists.
Python: module-level state IS a singleton.
# my_module.py
config = load_config() # module loaded once; config is "singleton"
For class-based:
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
But really, just use a module-level value. Singletons are often seen as anti-patterns anyway (global state).
Factory
GoF: encapsulate object creation.
Python: functions that return objects.
def create_user(role: str) -> User:
if role == "admin":
return AdminUser()
return RegularUser()
No AbstractFactory class needed.
Adapter
GoF: convert one interface to another.
Python: duck typing means you often don't need adapters.
class JsonResponse:
def to_dict(self): ...
class CsvResponse:
def to_dict(self): ...
# Any code that calls .to_dict() works with both. No adapter needed.
When you do need it: a class with a different method name calls the equivalent.
Patterns that still matter
Observer / Pub-Sub
For decoupling event producers and consumers:
class EventBus:
def __init__(self):
self._subscribers = []
def subscribe(self, callback):
self._subscribers.append(callback)
def publish(self, event):
for cb in self._subscribers:
cb(event)
bus = EventBus()
bus.subscribe(lambda e: print(f"Got {e}"))
bus.publish("hello")
For more sophisticated: blinker, or just async functions if working in asyncio.
Repository / DAO
Encapsulate data access:
class UserRepository:
def __init__(self, db):
self.db = db
def get_by_id(self, user_id: int) -> User:
return self.db.query(User).filter_by(id=user_id).first()
def create(self, user: User) -> None:
self.db.add(user)
self.db.commit()
Separates "how data is stored" from "what business logic does with it". Useful in larger codebases.
Command (functional alternative often better)
For undo/redo or task queues:
@dataclass
class CreateUserCommand:
name: str
email: str
def execute(self):
return User.create(self.name, self.email)
In simpler cases, a function + args tuple suffices.
Dependency Injection
Pass dependencies in; don't construct internally.
# Bad: hardcoded dependency
class UserService:
def __init__(self):
self.db = create_db_connection() # hard to test
# Good: injected
class UserService:
def __init__(self, db):
self.db = db # can pass test db
For larger apps: dependency-injector library or FastAPI's built-in DI.
Most Python apps don't need a DI framework; constructor injection by hand is enough.
SOLID in Python
The five principles still apply, though Python's flexibility softens some:
S — Single Responsibility
Each class/function does ONE thing. Easier to test and reuse.
O — Open/Closed
Open for extension; closed for modification. In Python: subclass or compose; don't edit core to add a new case.
L — Liskov Substitution
Subclasses can replace parents. In Python's duck typing, this is more about behavior than inheritance.
I — Interface Segregation
Many small interfaces > one large. Python doesn't have explicit interfaces, but the Protocol pattern fits:
class Readable(Protocol):
def read(self) -> str: ...
class Writable(Protocol):
def write(self, data: str) -> None: ...
Better than one big "Streamable" interface.
D — Dependency Inversion
Depend on abstractions, not concretions. Pass interfaces/Protocols, not concrete types.
Pythonic idioms
Use dataclass over plain classes for data
@dataclass
class User:
name: str
email: str
Cleaner than __init__ + __repr__ + __eq__ manually.
Use list/dict/set comprehensions
# Pythonic
squares = [x*x for x in numbers]
# Not pythonic
squares = []
for x in numbers:
squares.append(x*x)
Use unpacking
# Pythonic
a, b = b, a # swap
*rest, last = items
Use enumerate, zip, dict.items()
# Pythonic
for i, x in enumerate(items):
print(i, x)
for k, v in d.items():
...
Use defaultdict and Counter
from collections import defaultdict, Counter
counts = defaultdict(int)
for item in items:
counts[item] += 1
# Or simpler:
counts = Counter(items)
Anti-patterns to avoid
God object
One class doing everything. Split into smaller, focused classes.
Mutable default argument
def f(items=[]): # BAD
items.append(1)
return items
f() # [1]
f() # [1, 1] — same list!
# Fix:
def f(items=None):
if items is None:
items = []
items.append(1)
return items
The famous Python gotcha. Type-checkers warn.
Excessive inheritance
Deep class hierarchies are hard to follow. Prefer composition: instead of inheriting, hold an instance.
Reinventing the wheel
Python's stdlib + ecosystem covers most things. Search before writing.
Composition over inheritance
# Bad: deep hierarchy
class Animal: ...
class Mammal(Animal): ...
class Dog(Mammal): ...
class GuideDog(Dog): ...
# Better: composition
class Dog:
def __init__(self, skills: list[Skill]):
self.skills = skills
guide_dog = Dog(skills=[Sit(), Stay(), GuideHuman()])
More flexible; easier to test; easier to add skills.
When to use OOP heavily vs functional
- Heavy OOP: complex domain models, stateful services (e.g., a Connection class).
- Functional: data transformations, pipelines, business logic operating on simple data.
Most Python is hybrid: classes for state, functions for transformations.
Common pattern mistakes
- Overusing classes for plain data. Use dataclass.
- Forcing GoF patterns into Python. Python's features replace many of them.
- Deep inheritance. Prefer composition.
- God objects. Split responsibilities.
- Mutable default args. Classic bug.
Takeaway
Python's first-class functions and duck typing simplify or eliminate many classical OOP patterns. Strategy = function arg. Decorator = @decorator. Iterator = built-in. Singleton = module. Factory = function. Patterns that still matter: Observer/PubSub, Repository, Dependency Injection (lightweight), SOLID principles adapted. Pythonic style: dataclasses, comprehensions, composition over deep inheritance, mutable default args avoided.