Difficulty: Advanced
What are common design patterns in Python? How do they differ from traditional OOP patterns?
Python's dynamic nature simplifies many traditional design patterns. Some patterns are built into the language (Iterator, Decorator, Observer via signals).
Key patterns in Python: - Singleton: module-level variables or __new__ override - Factory: classmethod constructors or simple functions - Strategy: first-class functions replace strategy classes - Observer: callbacks, signals, or event systems - Decorator: built-in with @decorator syntax - Iterator: built-in with __iter__/__next__ or generators
Python's philosophy: use the simplest approach. Many GoF patterns exist to work around limitations in Java/C++ that Python doesn't have.
# Singleton via module (Pythonic way)
# config.py
# settings = Settings() # Module-level = singleton
# Singleton via __new__
class Singleton:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
a = Singleton()
b = Singleton()
print(a is b) # True - same instance
# Factory Method with classmethods
class Serializer:
def __init__(self, format_type):
self.format_type = format_type
@classmethod
def json(cls):
return cls('json')
@classmethod
def xml(cls):
return cls('xml')
@classmethod
def csv(cls):
return cls('csv')
s = Serializer.json() # Factory method
print(s.format_type) # 'json'
# Simple factory function (most Pythonic)
def create_connection(db_type, kwargs):
connections = {
'postgres': lambda: f"PostgreSQL({kwargs})",
'mysql': lambda: f"MySQL({kwargs})",
'sqlite': lambda: f"SQLite({kwargs})",
}
creator = connections.get(db_type)
if not creator:
raise ValueError(f"Unknown db: {db_type}")
return creator()
print(create_connection('postgres', host='localhost', port=5432))
In Python, modules are singletons naturally. Factory classmethods are cleaner than separate factory classes. A dict of callables replaces complex factory hierarchies.
# Traditional OOP: separate classes for each strategy
# Pythonic: just use functions!
from typing import Callable
# Sorting strategies as functions
def sort_by_name(items):
return sorted(items, key=lambda x: x['name'])
def sort_by_price_asc(items):
return sorted(items, key=lambda x: x['price'])
def sort_by_price_desc(items):
return sorted(items, key=lambda x: x['price'], reverse=True)
def sort_by_rating(items):
return sorted(items, key=lambda x: x['rating'], reverse=True)
# Context that uses the strategy
class ProductCatalog:
def __init__(self, products: list[dict]):
self.products = products
def display(self, sort_strategy: Callable = sort_by_name):
sorted_products = sort_strategy(self.products)
for p in sorted_products:
print(f" {p['name']}: ${p['price']:.2f} ({p['rating']} stars)")
products = [
{'name': 'Widget', 'price': 29.99, 'rating': 4.5},
{'name': 'Gadget', 'price': 49.99, 'rating': 3.8},
{'name': 'Doohickey', 'price': 9.99, 'rating': 4.9},
]
catalog = ProductCatalog(products)
print("By name:")
catalog.display(sort_by_name)
print("By price (high to low):")
catalog.display(sort_by_price_desc)
print("By rating:")
catalog.display(sort_by_rating)
In Python, first-class functions eliminate the need for strategy classes. Pass the function directly instead of wrapping it in an object.
# Observer with callbacks
class EventEmitter:
def __init__(self):
self._listeners: dict[str, list] = {}
def on(self, event: str, callback):
self._listeners.setdefault(event, []).append(callback)
return self # Allow chaining
def off(self, event: str, callback):
if event in self._listeners:
self._listeners[event].remove(callback)
def emit(self, event: str, *args, kwargs):
for callback in self._listeners.get(event, []):
callback(*args, kwargs)
# Usage
class UserService(EventEmitter):
def register(self, name, email):
user = {'name': name, 'email': email}
print(f"User registered: {name}")
self.emit('user_registered', user)
return user
# Observers (listeners)
def send_welcome_email(user):
print(f"Sending welcome email to {user['email']}")
def log_registration(user):
print(f"LOG: New user {user['name']} registered")
def update_analytics(user):
print(f"Analytics: Registration count updated")
service = UserService()
service.on('user_registered', send_welcome_email)
service.on('user_registered', log_registration)
service.on('user_registered', update_analytics)
service.register('Alice', 'alice@example.com')
The observer pattern decouples the event source from its handlers. Adding new behavior (analytics, logging) doesn't require modifying the UserService.
Singleton, Factory, Observer, Strategy, Decorator Pattern, Iterator Pattern