Python Design Patterns
Reusable Solutions for Common Problems
Master the classic design patterns in Python: creational, structural, and behavioral. Each pattern includes a practical example, stepβbyβstep explanation, and runnable code by @PoeticGlenn.
π Interactive Python Code Editor
Write, edit, and run Python code directly in your browser. No setup required β powered by Pyodide.
What Are Design Patterns and Why Do They Matter?
Design patterns are proven, reusable solutions to common software design problems. They are not finished code but templates that can be adapted to many different situations. Think of them as blueprints for writing more maintainable, flexible, and understandable code.
In the early 1990s, the "Gang of Four" (Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides) catalogued 23 classic patterns in their book Design Patterns: Elements of Reusable Object-Oriented Software. These patterns are divided into three categories:
- Creational β object creation mechanisms (Singleton, Factory, Builder, Prototype, Abstract Factory, Factory Method)
- Structural β composition of classes and objects (Adapter, Decorator, Proxy, Facade)
- Behavioral β communication between objects (Observer, Strategy, Command, Template Method, Iterator, Chain of Responsibility, State)
In this tutorial, you'll learn 17 essential patterns with practical Python examples. Each pattern is explained with:
- Why this pattern is used β the problem it solves
- How it operates β the mechanics of the pattern
- Its significance and role β how it contributes to better software
- How it affects the output β what you see when you run the code
By the end, you'll be able to recognise when to apply each pattern and implement them confidently in your own projects.
π Table of Contents
Jump to any pattern β all entries are equal size
π Singleton Pattern
Ensures a class has only one instance and provides a global point of access to it.
# Singleton using metaclass class SingletonMeta(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super().__call__(*args, **kwargs) return cls._instances[cls] class DatabaseConnection(metaclass=SingletonMeta): def __init__(self): self.connected = False print("Creating database connection...") def connect(self): if not self.connected: self.connected = True print("Connected to database") return "Connection established" db1 = DatabaseConnection() db2 = DatabaseConnection() print(db1 is db2) # Singleton using decorator def singleton(cls): instances = {} def get_instance(*args, **kwargs): if cls not in instances: instances[cls] = cls(*args, **kwargs) return instances[cls] return get_instance @singleton class ConfigManager: def __init__(self): self.settings = {"debug": True, "version": "1.0"} config1 = ConfigManager() config2 = ConfigManager() print(config1 is config2)
SingletonMeta) that overrides __call__ to intercept instance creation. It stores a dictionary of instances keyed by class. When a class is instantiated, it checks if an instance already exists; if not, it creates one and stores it. The decorator version wraps the class with a function that does the same.
print(db1 is db2) outputs True, proving both variables reference the same instance. The constructor message "Creating database connection..." appears only once, demonstrating that the instance is created only once.
π Factory Pattern
Creates objects without specifying the exact class. Provides a common interface for object creation.
from abc import ABC, abstractmethod # Product interface class Vehicle(ABC): @abstractmethod def drive(self): pass @abstractmethod def honk(self): pass # Concrete products class Car(Vehicle): def drive(self): return "Driving a car on the road" def honk(self): return "Beep beep!" class Bike(Vehicle): def drive(self): return "Riding a bike on the trail" def honk(self): return "Ring ring!" class Truck(Vehicle): def drive(self): return "Driving a truck on the highway" def honk(self): return "Honk honk!" # Factory class VehicleFactory: def create_vehicle(self, vehicle_type): if vehicle_type == "car": return Car() elif vehicle_type == "bike": return Bike() elif vehicle_type == "truck": return Truck() else: raise ValueError(f"Unknown vehicle type: {vehicle_type}") factory = VehicleFactory() car = factory.create_vehicle("car") bike = factory.create_vehicle("bike") print(car.drive()) print(car.honk()) print(bike.drive())
VehicleFactory) has a method that takes a parameter (e.g., "car") and returns the corresponding product object. The client calls this method without knowing the internal logic of object creation.
car.drive(), car.honk(), bike.drive()), demonstrating that the correct objects are created based on the input.
π¨ Builder Pattern
Separates the construction of a complex object from its representation, allowing step-by-step construction.
class Computer: def __init__(self): self.cpu = None self.ram = None self.storage = None self.gpu = None self.os = None def __str__(self): return f"Computer(CPU={self.cpu}, RAM={self.ram}, Storage={self.storage}, GPU={self.gpu}, OS={self.os})" class ComputerBuilder: def __init__(self): self.computer = Computer() def set_cpu(self, cpu): self.computer.cpu = cpu return self def set_ram(self, ram): self.computer.ram = ram return self def set_storage(self, storage): self.computer.storage = storage return self def set_gpu(self, gpu): self.computer.gpu = gpu return self def set_os(self, os): self.computer.os = os return self def build(self): return self.computer # Director class ComputerDirector: def build_gaming_computer(self): builder = ComputerBuilder() return (builder .set_cpu("Intel i9") .set_ram("32GB DDR5") .set_storage("2TB NVMe SSD") .set_gpu("RTX 4090") .set_os("Windows 11") .build()) def build_office_computer(self): builder = ComputerBuilder() return (builder .set_cpu("Intel i5") .set_ram("16GB DDR4") .set_storage("512GB SSD") .set_os("Ubuntu") .build()) director = ComputerDirector() gaming_pc = director.build_gaming_computer() office_pc = director.build_office_computer() print(gaming_pc) print(office_pc)
self to enable method chaining. A director orchestrates the construction steps to produce specific configurations.
𧬠Prototype Pattern
Creates new objects by cloning existing objects, avoiding the cost of creating from scratch.
import copy from abc import ABC, abstractmethod class Prototype(ABC): @abstractmethod def clone(self): pass class Document(Prototype): def __init__(self, title, content, author): self.title = title self.content = content self.author = author self.metadata = {"created": "2026-01-01", "version": 1} def clone(self): return copy.deepcopy(self) def __str__(self): return f"Document(title='{self.title}', content='{self.content[:30]}...', author='{self.author}')" original = Document("Report 2026", "This is the full content of the report...", "Glenn") clone1 = original.clone() clone1.title = "Report 2026 - Copy" clone1.metadata["version"] = 2 clone2 = original.clone() clone2.title = "Report 2026 - Draft" print(original) print(clone1) print(clone2) print(clone1.metadata)
clone() method. Concrete prototypes implement it using copy.deepcopy() to create a full copy. Clients can then clone the prototype and modify the copy as needed.
clone1 does not affect original or clone2.
π Adapter Pattern
Allows incompatible interfaces to work together by acting as a bridge between them.
# Target interface class PaymentProcessor: def process_payment(self, amount, currency): return f"Processing {amount} {currency}" # Incompatible third-party service class StripeAPI: def create_charge(self, amount_in_cents, currency_code): return f"Stripe charge: {amount_in_cents} cents in {currency_code}" # Adapter class StripeAdapter(PaymentProcessor): def __init__(self, stripe_api): self.stripe_api = stripe_api def process_payment(self, amount, currency): amount_in_cents = int(amount * 100) return self.stripe_api.create_charge(amount_in_cents, currency) class PayPalAPI: def send_payment(self, amount_usd): return f"PayPal payment: ${amount_usd}" class PayPalAdapter(PaymentProcessor): def __init__(self, paypal_api): self.paypal_api = paypal_api def process_payment(self, amount, currency): if currency != "USD": amount = amount * 1.1 return self.paypal_api.send_payment(amount) # Usage stripe = StripeAdapter(StripeAPI()) paypal = PayPalAdapter(PayPalAPI()) print(stripe.process_payment(25.50, "USD")) print(paypal.process_payment(25.50, "EUR"))
π¨ Decorator Pattern
Dynamically adds behaviour to objects without modifying their structure. (This is the same as Python's built-in decorators but implemented as a pattern.)
from abc import ABC, abstractmethod # Component interface class Coffee(ABC): @abstractmethod def cost(self): pass @abstractmethod def description(self): pass # Concrete component class SimpleCoffee(Coffee): def cost(self): return 2.0 def description(self): return "Simple coffee" # Decorator base class class CoffeeDecorator(Coffee): def __init__(self, coffee): self._coffee = coffee def cost(self): return self._coffee.cost() def description(self): return self._coffee.description() # Concrete decorators class Milk(CoffeeDecorator): def cost(self): return self._coffee.cost() + 0.5 def description(self): return self._coffee.description() + ", milk" class Sugar(CoffeeDecorator): def cost(self): return self._coffee.cost() + 0.2 def description(self): return self._coffee.description() + ", sugar" class WhippedCream(CoffeeDecorator): def cost(self): return self._coffee.cost() + 0.7 def description(self): return self._coffee.description() + ", whipped cream" # Usage coffee = SimpleCoffee() coffee = Milk(coffee) coffee = Sugar(coffee) coffee = WhippedCream(coffee) print(coffee.description()) print(f"Total cost: ${coffee.cost():.2f}")
π‘οΈ Proxy Pattern
Provides a surrogate or placeholder for another object to control access, add logging, or implement lazy loading.
from abc import ABC, abstractmethod class Image(ABC): @abstractmethod def display(self): pass class RealImage(Image): def __init__(self, filename): self.filename = filename self._load_from_disk() def _load_from_disk(self): print(f"Loading image from disk: {self.filename}") def display(self): return f"Displaying image: {self.filename}" class ImageProxy(Image): def __init__(self, filename): self.filename = filename self._real_image = None def display(self): if self._real_image is None: self._real_image = RealImage(self.filename) return self._real_image.display() # Proxy with access control class ProtectedImageProxy(Image): def __init__(self, filename, user_role): self.filename = filename self.user_role = user_role self._real_image = None def display(self): if self.user_role != "admin": return "Access denied: insufficient permissions" if self._real_image is None: self._real_image = RealImage(self.filename) return self._real_image.display() # Usage image1 = ImageProxy("photo.jpg") image2 = ProtectedImageProxy("secret.jpg", "guest") image3 = ProtectedImageProxy("secret.jpg", "admin") print(image1.display()) print(image2.display()) print(image3.display())
ImageProxy provides lazy loading; ProtectedImageProxy adds access control.
image1, the image loads only when display() is called (lazy). For image2, access is denied for a guest; for image3, an admin can view the image. The output shows these different behaviours.
ποΈ Facade Pattern
Provides a simplified interface to a complex subsystem, making it easier to use.
# Complex subsystem class CPU: def start(self): return "CPU started" def execute(self, instruction): return f"Executing: {instruction}" class Memory: def load(self, address, data): return f"Loading {data} at {address}" def read(self, address): return f"Reading from {address}" class HardDrive: def read(self, sector): return f"Reading sector {sector}" def write(self, sector, data): return f"Writing {data} to sector {sector}" # Facade class ComputerFacade: def __init__(self): self.cpu = CPU() self.memory = Memory() self.hard_drive = HardDrive() def start_computer(self): result = [] result.append(self.cpu.start()) result.append(self.memory.load("0x0000", "bootloader")) result.append(self.hard_drive.read("boot_sector")) result.append(self.cpu.execute("boot sequence")) result.append("Computer ready") return result def shutdown(self): return ["Saving state", "Shutting down", "Power off"] # Usage computer = ComputerFacade() for step in computer.start_computer(): print(step)
π Observer Pattern
Defines a one-to-many dependency between objects so that when one changes state, all dependents are notified.
from abc import ABC, abstractmethod class Observer(ABC): @abstractmethod def update(self, data): pass class Subject: def __init__(self): self._observers = [] self._state = None def attach(self, observer): self._observers.append(observer) def detach(self, observer): self._observers.remove(observer) def notify(self): for observer in self._observers: observer.update(self._state) def set_state(self, state): self._state = state self.notify() # Concrete observers class EmailNotifier(Observer): def update(self, data): return f"Email: State changed to {data}" class SMSNotifier(Observer): def update(self, data): return f"SMS: State changed to {data}" class Logger(Observer): def update(self, data): return f"Log: State changed to {data}" # Usage subject = Subject() email = EmailNotifier() sms = SMSNotifier() logger = Logger() subject.attach(email) subject.attach(sms) subject.attach(logger) print("Setting state to 'active':") subject.set_state("active") print("\nSetting state to 'inactive':") subject.set_state("inactive")
update() method on each observer, passing the new state.
π― Strategy Pattern
Defines a family of algorithms, encapsulates each one, and makes them interchangeable at runtime.
from abc import ABC, abstractmethod # Strategy interface class SortStrategy(ABC): @abstractmethod def sort(self, data): pass # Concrete strategies class BubbleSort(SortStrategy): def sort(self, data): arr = data.copy() n = len(arr) for i in range(n): for j in range(0, n - i - 1): if arr[j] > arr[j + 1]: arr[j], arr[j + 1] = arr[j + 1], arr[j] return arr class QuickSort(SortStrategy): def sort(self, data): arr = data.copy() if len(arr) <= 1: return arr pivot = arr[len(arr) // 2] left = [x for x in arr if x < pivot] middle = [x for x in arr if x == pivot] right = [x for x in arr if x > pivot] return self.sort(left) + middle + self.sort(right) class MergeSort(SortStrategy): def sort(self, data): arr = data.copy() if len(arr) <= 1: return arr mid = len(arr) // 2 left = self.sort(arr[:mid]) right = self.sort(arr[mid:]) return self._merge(left, right) def _merge(self, left, right): result = [] i, j = 0, 0 while i < len(left) and j < len(right): if left[i] <= right[j]: result.append(left[i]) i += 1 else: result.append(right[j]) j += 1 result.extend(left[i:]) result.extend(right[j:]) return result # Context class DataProcessor: def __init__(self, strategy): self._strategy = strategy def set_strategy(self, strategy): self._strategy = strategy def process(self, data): return self._strategy.sort(data) # Usage data = [64, 34, 25, 12, 22, 11, 90] processor = DataProcessor(BubbleSort()) print(processor.process(data)) processor.set_strategy(QuickSort()) print(processor.process(data)) processor.set_strategy(MergeSort()) print(processor.process(data))
π Command Pattern
Encapsulates a request as an object, allowing parameterization of clients with different requests and supporting undo operations.
from abc import ABC, abstractmethod # Command interface class Command(ABC): @abstractmethod def execute(self): pass @abstractmethod def undo(self): pass # Receiver class Light: def __init__(self, location): self.location = location self.is_on = False def turn_on(self): self.is_on = True return f"{self.location} light is ON" def turn_off(self): self.is_on = False return f"{self.location} light is OFF" # Concrete commands class TurnOnCommand(Command): def __init__(self, light): self.light = light def execute(self): return self.light.turn_on() def undo(self): return self.light.turn_off() class TurnOffCommand(Command): def __init__(self, light): self.light = light def execute(self): return self.light.turn_off() def undo(self): return self.light.turn_on() # Invoker class RemoteControl: def __init__(self): self._history = [] def execute_command(self, command): result = command.execute() self._history.append(command) return result def undo_last(self): if self._history: command = self._history.pop() return command.undo() return "Nothing to undo" # Usage living_room = Light("Living Room") kitchen = Light("Kitchen") remote = RemoteControl() print(remote.execute_command(TurnOnCommand(living_room))) print(remote.execute_command(TurnOffCommand(kitchen))) print(remote.execute_command(TurnOnCommand(kitchen))) print(remote.undo_last()) print(remote.undo_last()) print(remote.undo_last())
π Template Method Pattern
Defines the skeleton of an algorithm in a method, deferring some steps to subclasses.
from abc import ABC, abstractmethod class DataProcessor(ABC): def process(self, data): if not self._validate(data): return "Validation failed" cleaned = self._clean(data) transformed = self._transform(cleaned) return self._output(transformed) @abstractmethod def _validate(self, data): pass @abstractmethod def _clean(self, data): pass @abstractmethod def _transform(self, data): pass @abstractmethod def _output(self, data): pass class CSVProcessor(DataProcessor): def _validate(self, data): return isinstance(data, list) and len(data) > 0 def _clean(self, data): return [str(x).strip() for x in data if x is not None] def _transform(self, data): return ",".join(data) def _output(self, data): return f"CSV Output: {data}" class JSONProcessor(DataProcessor): def _validate(self, data): return isinstance(data, dict) def _clean(self, data): return {k: str(v).strip() for k, v in data.items()} def _transform(self, data): return {k: v.upper() for k, v in data.items()} def _output(self, data): return f"JSON Output: {data}" # Usage csv_data = ["Apple", " Banana ", None, " Cherry"] json_data = {"name": " Glenn ", "city": " Manila ", "country": " Philippines"} csv_processor = CSVProcessor() json_processor = JSONProcessor() print(csv_processor.process(csv_data)) print(json_processor.process(json_data))
π Iterator Pattern
Provides a way to access elements of an aggregate object sequentially without exposing its underlying representation.
from abc import ABC, abstractmethod class Iterator(ABC): @abstractmethod def has_next(self): pass @abstractmethod def next(self): pass class Aggregate(ABC): @abstractmethod def create_iterator(self): pass class Book: def __init__(self, title, author): self.title = title self.author = author def __str__(self): return f"'{self.title}' by {self.author}" class BookCollection(Aggregate): def __init__(self): self._books = [] def add_book(self, book): self._books.append(book) def create_iterator(self): return BookIterator(self._books) class BookIterator(Iterator): def __init__(self, books): self._books = books self._index = 0 def has_next(self): return self._index < len(self._books) def next(self): if self.has_next(): book = self._books[self._index] self._index += 1 return book raise StopIteration # Reverse iterator class ReverseBookIterator(Iterator): def __init__(self, books): self._books = books self._index = len(books) - 1 def has_next(self): return self._index >= 0 def next(self): if self.has_next(): book = self._books[self._index] self._index -= 1 return book raise StopIteration # Usage collection = BookCollection() collection.add_book(Book("1984", "George Orwell")) collection.add_book(Book("Brave New World", "Aldous Huxley")) collection.add_book(Book("Fahrenheit 451", "Ray Bradbury")) print("Forward iteration:") iterator = collection.create_iterator() while iterator.has_next(): print(iterator.next()) print("\nReverse iteration:") reverse_iterator = ReverseBookIterator(collection._books) while reverse_iterator.has_next(): print(reverse_iterator.next())
has_next() and next() methods to traverse the elements sequentially.
π’ Factory Method Pattern
Defines an interface for creating an object, but lets subclasses decide which class to instantiate.
from abc import ABC, abstractmethod # Product interface class Transport(ABC): @abstractmethod def deliver(self): pass # Concrete products class Truck(Transport): def deliver(self): return "Delivering by truck (land delivery)" class Ship(Transport): def deliver(self): return "Delivering by ship (sea delivery)" class Plane(Transport): def deliver(self): return "Delivering by plane (air delivery)" # Creator abstract class class Logistics(ABC): @abstractmethod def create_transport(self) -> Transport: pass def plan_delivery(self): transport = self.create_transport() return f"Planning delivery: {transport.deliver()}" # Concrete creators class RoadLogistics(Logistics): def create_transport(self) -> Transport: return Truck() class SeaLogistics(Logistics): def create_transport(self) -> Transport: return Ship() class AirLogistics(Logistics): def create_transport(self) -> Transport: return Plane() # Usage road = RoadLogistics() sea = SeaLogistics() air = AirLogistics() print(road.plan_delivery()) print(sea.plan_delivery()) print(air.plan_delivery())
create_transport() to return the appropriate transport.
π’ Abstract Factory Pattern
Provides an interface for creating families of related or dependent objects without specifying concrete classes.
from abc import ABC, abstractmethod # Abstract products class Chair(ABC): @abstractmethod def sit_on(self): pass class Sofa(ABC): @abstractmethod def lie_on(self): pass # Concrete products - Victorian class VictorianChair(Chair): def sit_on(self): return "Sitting on a Victorian chair" class VictorianSofa(Sofa): def lie_on(self): return "Lying on a Victorian sofa" # Concrete products - Modern class ModernChair(Chair): def sit_on(self): return "Sitting on a modern chair" class ModernSofa(Sofa): def lie_on(self): return "Lying on a modern sofa" # Concrete products - Art Deco class ArtDecoChair(Chair): def sit_on(self): return "Sitting on an Art Deco chair" class ArtDecoSofa(Sofa): def lie_on(self): return "Lying on an Art Deco sofa" # Abstract Factory class FurnitureFactory(ABC): @abstractmethod def create_chair(self) -> Chair: pass @abstractmethod def create_sofa(self) -> Sofa: pass # Concrete factories class VictorianFurnitureFactory(FurnitureFactory): def create_chair(self) -> Chair: return VictorianChair() def create_sofa(self) -> Sofa: return VictorianSofa() class ModernFurnitureFactory(FurnitureFactory): def create_chair(self) -> Chair: return ModernChair() def create_sofa(self) -> Sofa: return ModernSofa() class ArtDecoFurnitureFactory(FurnitureFactory): def create_chair(self) -> Chair: return ArtDecoChair() def create_sofa(self) -> Sofa: return ArtDecoSofa() # Usage def furnish_room(factory: FurnitureFactory): chair = factory.create_chair() sofa = factory.create_sofa() return [chair.sit_on(), sofa.lie_on()] victorian = VictorianFurnitureFactory() modern = ModernFurnitureFactory() artdeco = ArtDecoFurnitureFactory() print("Victorian:", furnish_room(victorian)) print("Modern:", furnish_room(modern)) print("Art Deco:", furnish_room(artdeco))
π Chain of Responsibility
Passes a request along a chain of handlers, where each handler decides to process it or pass it to the next.
from abc import ABC, abstractmethod class Handler(ABC): def __init__(self): self._next = None def set_next(self, handler): self._next = handler return handler @abstractmethod def handle(self, request): if self._next: return self._next.handle(request) return "Request unhandled" class AuthenticationHandler(Handler): def handle(self, request): if not request.get("token"): return "Authentication failed: no token" print("Authentication passed") return super().handle(request) class AuthorizationHandler(Handler): def handle(self, request): if request.get("role") not in ["admin", "user"]: return "Authorization failed: invalid role" print("Authorization passed") return super().handle(request) class ValidationHandler(Handler): def handle(self, request): if not request.get("data"): return "Validation failed: no data" print("Validation passed") return super().handle(request) class ProcessingHandler(Handler): def handle(self, request): data = request.get("data") return f"Processing complete: {data.upper()}" # Usage chain = AuthenticationHandler() chain.set_next(AuthorizationHandler()).set_next(ValidationHandler()).set_next(ProcessingHandler()) request1 = {"token": "abc123", "role": "admin", "data": "important"} request2 = {"token": "abc123", "role": "guest", "data": "important"} request3 = {"role": "admin", "data": "important"} print(chain.handle(request1)) print(chain.handle(request2)) print(chain.handle(request3))
π¦ State Pattern
Allows an object to alter its behaviour when its internal state changes. The object will appear to change its class.
from abc import ABC, abstractmethod class State(ABC): @abstractmethod def handle(self, context): pass class LockedState(State): def handle(self, context): context.state = UnlockedState() return "Phone unlocked" class UnlockedState(State): def handle(self, context): context.state = LockedState() return "Phone locked" class Context: def __init__(self, state): self.state = state def request(self): return self.state.handle(self) # More complex example class TrafficLight: def __init__(self): self.state = RedLight() def change(self): return self.state.change(self) class RedLight: def change(self, light): light.state = GreenLight() return "Red β Green: Go!" class GreenLight: def change(self, light): light.state = YellowLight() return "Green β Yellow: Caution!" class YellowLight: def change(self, light): light.state = RedLight() return "Yellow β Red: Stop!" # Usage phone = Context(LockedState()) print(phone.request()) print(phone.request()) traffic = TrafficLight() for _ in range(6): print(traffic.change())
request() toggles the state, outputting "Phone unlocked" then "Phone locked". For the traffic light, it cycles through the six transitions, printing each step.
These design patterns provide reusable solutions to common problems in Python. Practice each pattern with the runnable examples above.
π You've Mastered Python Design Patterns
From Singleton to State, you've explored 17 essential design patterns that will make your code more flexible, maintainable, and professional. Each pattern addresses a specific problem and provides a proven blueprint for solving it.
Remember, design patterns are tools, not rules. Use them when they fit your context, and don't force them where simpler solutions exist. The best way to internalise them is to practice β try modifying the examples, combine patterns, and apply them to your own projects.
If you found this guide helpful, share it with your network and follow @PoeticGlenn for more Python insights.