- 1 Three major categories of Python design patterns include: Creational (Creation of objects), Structural (composition of objects) and Behavioral (interaction between objects)
- 2 The Singleton pattern can be seen in parts of Python, such as the logging module, while Django uses a registry-style approach to manage database connections.
- 3 Decorator pattern in Python comes with the native language syntax – through decorators @decorator making it the most Pythonic design pattern
- 4 Using the right design patterns helps developers write cleaner and more organized code, making applications easier to maintain, scale, and debug over time.
- 5 The cardinal sin is over-engineering by using design patterns instead of classes or functions. Design patterns should solve a problem not be applied automatically
In programming with Python, design patterns are considered as approaches to solve problems arising in software design. The first description of the patterns was given in 1994, in the book Design Patterns: Elements of Reusable Object-Oriented Software by Gamma, Helm, Johnson, Vlissides – “the Gang of Four“. The 23 patterns were reformulated and adapted to Python syntax and coding standards – thus becoming much more concise and elegant compared to the original patterns in Java or C++.
This article is going to present the list of 10 main Python design patterns, including Creational, Structural and Behavioral ones, giving their definitions, code examples, use cases, and discussing situations where such design pattern is not recommended to be implemented.
Why Design Patterns Matter in Python
The flexibility of Python is its strong and weak side. Python does not enforce interface contracts at the language level the way Java does, though modules like abc and typing.Protocol provide optional mechanisms for defining interface-like behavior. This flexibility allows developers to use multiple different implementations to achieve the same result. Design patterns are the guidelines that ensure the Python application has enough code to develop and test the project by a team of specialists.
The design patterns provide the opportunity to use common names for discussing class interaction types: Factory, Adapter, Decorator, and so forth. All these terms are known to any programmer working with Python, which facilitates communication.
Three main advantages of applying design patterns in Python:
- Readability – The developer who knows the meaning of the Singleton and Observer patterns understands the code regardless of the exact approach used
- Testability – Strategy and Factory patterns separate logic from implementation, which simplifies unit testing greatly
- Scalability – Facade and Adapter structural patterns ensure system growth without any modifications of the tested code
Creational Patterns – Controlling How Objects are Created
The creational design pattern abstracts the object creation process, thus enabling you to have full control over what objects are created, how they are created, and when they are created.
1. Singleton Pattern
What it does: To ensure the singleton is created once and provides a global point of access.
Real-world use: Database connection manager of Django, logger implementation in Python, configuration managers, and thread pool managers.
When to use: The singleton design pattern should be used when there is a need to have exactly one object coordinating actions in a whole application/system; often related to common resource access such as database connections or configuration.
When NOT to use: If it brings hidden coupling into your design that can complicate unit tests.
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Singleton: _instance = None def __new__(cls, *args, **kwargs): if not cls._instance: cls._instance = super().__new__(cls, *args, **kwargs) return cls._instance # Usage instance1 = Singleton() instance2 = Singleton() print(instance1 is instance2) # True — same object |
2. Factory Pattern
What it does: An abstract interface for the creation of objects where the decision regarding which concrete class to create remains up to subclasses or runtime decisions.
Real-world use: The Flask framework uses the Factory pattern when creating application instances (create_app()). The creation of form and model fields in Django uses Factory principles.
When to use: When the creation of object types is decided upon during runtime or needs to be decoupled from its usage.
When NOT to use: When only one type of object needs to be created. The Factory pattern introduces an extra layer of abstraction; this comes with added costs only when necessary.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | class Dog: def speak(self): return "Woof!" class Cat: def speak(self): return "Meow!" class AnimalFactory: @staticmethod def create_animal(animal_type: str): animals = {"dog": Dog, "cat": Cat} animal_class = animals.get(animal_type.lower()) if not animal_class: raise ValueError(f"Unknown animal type: {animal_type}") return animal_class() # Usage animal = AnimalFactory.create_animal("dog") print(animal.speak()) # Woof! |
3. Abstract Factory Pattern
What it does: It defines an interface for creating families of related or dependent objects without specifying their concrete classes.
Real-world use: Toolkit libraries for user interfaces, where they have to create widget families like buttons, check boxes, and text fields for various operating systems.
When to use: It is best used when your application works with more than one family of objects.
When NOT to use: It should be avoided if there is just one family of products. This pattern is very complex and should be used only when required.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | from abc import ABC, abstractmethod class Button(ABC): @abstractmethod def render(self): pass class WindowsButton(Button): def render(self): return "Rendering Windows button" class MacButton(Button): def render(self): return "Rendering Mac button" class GUIFactory(ABC): @abstractmethod def create_button(self) -> Button: pass class WindowsFactory(GUIFactory): def create_button(self) -> Button: return WindowsButton() class MacFactory(GUIFactory): def create_button(self) -> Button: return MacButton() # Usage factory = WindowsFactory() button = factory.create_button() print(button.render()) # Rendering Windows button |
4. Builder Pattern
What it does: It distinguishes between the object construction and its representation, providing the ability to construct the same object in various ways.
Real-world use: When building SQL statements programmatically, constructing HTTP requests with optional parameters, or assembling complex objects in some other way.
When to use: Object creation is rather complicated because it consists of multiple steps or numerous optional parameters (10 or more).
When NOT to use: Object creation is rather trivial. If your object has just 2-3 fields, then you may do fine with a regular constructor or even a dataclass.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | class QueryBuilder: def __init__(self): self._table = "" self._conditions = [] self._limit = None def from_table(self, table: str): self._table = table return self def where(self, condition: str): self._conditions.append(condition) return self def limit(self, count: int): self._limit = count return self def build(self) -> str: query = f"SELECT * FROM {self._table}" if self._conditions: query += " WHERE " + " AND ".join(self._conditions) if self._limit: query += f" LIMIT {self._limit}" return query # Usage query = ( QueryBuilder() .from_table("users") .where("age > 18") .where("active = true") .limit(10) .build() ) print(query) # SELECT * FROM users WHERE age > 18 AND active = true LIMIT 10 |
5. Prototype Pattern
What it does: Constructs a new object through the copy of another existing object (the prototype), as opposed to creating a new one entirely.
Real-world use: Copying configuration objects, duplicating game entities, and copying document templates, which might be costly to fully initialize.
When to use: Whenever creating objects is expensive, such as in the case of databases or other costly operations, and where a number of objects are needed.
When NOT to use: For simple objects that are easy to instantiate.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | import copy class Config: def __init__(self, settings: dict): self.settings = settings def clone(self): return copy.deepcopy(self) # Usage base_config = Config({"debug": False, "db_host": "localhost", "timeout": 30}) prod_config = base_config.clone() prod_config.settings["db_host"] = "prod-db.example.com" print(base_config.settings["db_host"]) # localhost print(prod_config.settings["db_host"]) # prod-db.example.com |
Structural Patterns — Designing Objects and Classes
This pattern focuses on the structural aspects of building complex systems using classes and objects to make sure that systems are both flexible and efficient.
6. Adapter Pattern
What it does: Makes interfaces of unrelated classes compatible by creating a wrapper for one class which implements an interface expected by the client.
Real-world use: Used to integrate third-party libraries that have incompatible interfaces, adapting legacy code with a new interface, or connecting to external systems with different interfaces.
When to use: When it’s necessary to use a class with an inappropriate interface but it’s impossible to change it.
When NOT to use: When one has access to the original code of the interfaces. Adapter uses a level of indirection which isn’t necessary in other cases.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class EuropeanSocket: def voltage(self): return 230 def live(self): return "L" class USASocket: def voltage(self): return 120 def neutral(self): return "N" class EuropeanToUSAAdapter: def __init__(self, socket: EuropeanSocket): self._socket = socket def voltage(self): return 120 # Convert voltage def neutral(self): return self._socket.live() # Map interface # Usage european_socket = EuropeanSocket() adapter = EuropeanToUSAAdapter(european_socket) print(adapter.voltage()) # 120 print(adapter.neutral()) # L |
7. Decorator Pattern
What it does: Allows adding new functionality or behaviors to an object dynamically, without modifying its structure or by creating subclasses.
Real-world use: Python built-ins like property(), staticmethod(), classmethod(), Flask @app.route, Django @login_required – decorator pattern is part of Python itself.
When to use: Use when you want to add behaviors to objects on-the-fly without changing others of the same class type.
When NOT to use: When using multiple decorators leads to complex code paths that are difficult to debug. More than three to four decorators per function is a red flag.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | import functools import time def timer(func): @functools.wraps(func) def wrapper(*args, **kwargs): start = time.perf_counter() result = func(*args, **kwargs) end = time.perf_counter() print(f"{func.__name__} ran in {end - start:.4f}s") return result return wrapper def log_call(func): @functools.wraps(func) def wrapper(*args, **kwargs): print(f"Calling {func.__name__} with args={args}") return func(*args, **kwargs) return wrapper @timer @log_call def fetch_data(user_id: int): time.sleep(0.1) # Simulate DB call return {"user_id": user_id, "name": "Alice"} result = fetch_data(42) # Calling fetch_data with args=(42,) # fetch_data ran in 0.1003s |
8. Facade Pattern
What it does: It gives a simple interface to a more complex subsystem while concealing its complexity from the user.
Real-world use: Django’s ORM works as a Facade for raw SQL. Requests module works as a Facade for Python’s underlying urllib module. Wrappers for payment gateway services such as Stripe and PayPal.
When to use: In cases where a subsystem is overly complicated but requires an elegant interface for its basic functionalities.
When NOT to use: Where there is no genuine need for hiding the complexities of a system from the user. Overly complicated Facades lead to circumvention by users, resulting in a less usable design.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | class CPU: def freeze(self): print("CPU: Freeze") def jump(self, address): print(f"CPU: Jump to {address}") def execute(self): print("CPU: Execute") class Memory: def load(self, address, data): print(f"Memory: Load {data} at {address}") class HardDrive: def read(self, sector, size): return f"Data from sector {sector}" class ComputerFacade: """Simplified interface hiding CPU, Memory, HardDrive complexity.""" def __init__(self): self.cpu = CPU() self.memory = Memory() self.hard_drive = HardDrive() def start(self): self.cpu.freeze() data = self.hard_drive.read(sector=0, size=1024) self.memory.load(address=0, data=data) self.cpu.jump(address=0) self.cpu.execute() # Usage — client only needs one method computer = ComputerFacade() computer.start() |
Behavioral Patterns — Managing Communication Between Objects
Behavioral patterns specify how objects will interact and communicate among themselves, sharing responsibilities within a system.
9. Strategy Pattern
What it does: This pattern defines an algorithm family, encapsulates each algorithm separately, and allows them to be interchanged without modifying client code.
Real-world use: Sorting algorithm implementations, where a different sorting algorithm can be chosen depending on the type of data to be sorted; e.g., quick sort or merge sort. Other practical examples include switching payment methods such as credit card payments using Stripe, PayPal, or bank transfer services.
When to use: If there are several ways to perform a task (different algorithms), then strategy may come into play.
When NOT to use: If there is only one way to perform a certain task and that algorithm will never change in the future, the strategy pattern is not applicable.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | from abc import ABC, abstractmethod class SortStrategy(ABC): @abstractmethod def sort(self, data: list) -> list: pass class BubbleSort(SortStrategy): def sort(self, data: list) -> list: data = data.copy() n = len(data) for i in range(n): for j in range(0, n - i - 1): if data[j] > data[j + 1]: data[j], data[j + 1] = data[j + 1], data[j] return data class PythonBuiltinSort(SortStrategy): def sort(self, data: list) -> list: return sorted(data) class Sorter: def __init__(self, strategy: SortStrategy): self._strategy = strategy def set_strategy(self, strategy: SortStrategy): self._strategy = strategy def sort(self, data: list) -> list: return self._strategy.sort(data) # Usage data = [5, 2, 8, 1, 9] sorter = Sorter(PythonBuiltinSort()) print(sorter.sort(data)) # [1, 2, 5, 8, 9] sorter.set_strategy(BubbleSort()) print(sorter.sort(data)) # [1, 2, 5, 8, 9] |
10. Observer Pattern
What it does: Establishes a one-to-many relationship between objects such that when one object (subject) changes state, all objects dependent on it (observers) are notified automatically.
Real-world use: Django signals (post_save, pre_delete), event-based graphical user interfaces, real-time notification services, and stock price alerts.
When to use: Use when a change in one object necessitates changes in other objects and the number of objects affected is not known in advance.
When NOT to use: When there are too many objects and the sequence of notifications becomes unpredictable. It may become quite hard to trace which object caused a particular notification in such situations.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | from abc import ABC, abstractmethod class Observer(ABC): @abstractmethod def update(self, event: str, data: dict): pass class Subject: def __init__(self): self._observers: list[Observer] = [] def attach(self, observer: Observer): self._observers.append(observer) def detach(self, observer: Observer): self._observers.remove(observer) def notify(self, event: str, data: dict): for observer in self._observers: observer.update(event, data) class EmailNotifier(Observer): def update(self, event: str, data: dict): print(f"Email sent for event '{event}': {data}") class LogNotifier(Observer): def update(self, event: str, data: dict): print(f"Log recorded for event '{event}': {data}") # Usage order_system = Subject() order_system.attach(EmailNotifier()) order_system.attach(LogNotifier()) order_system.notify("order_placed", {"order_id": 101, "amount": 250.00}) # Email sent for event 'order_placed': {'order_id': 101, 'amount': 250.0} # Log recorded for event 'order_placed': {'order_id': 101, 'amount': 250.0} |
Using Python Code Checkers to Ensure Clean Pattern Implementation
The use of design patterns results in abstraction, and abstractions build up complexity over time. Python code checkers help ensure that the implementations of such patterns adhere to coding standards and remain readable, consistent, and maintainable across different teams.
| Tool | Type | What it enforces |
| Pylint | Linter | PEP 8 style issues, unused variables, code smells, and maintainability concerns |
| Flake8 | Linter | Style violations, line length issues, and import formatting checks |
| Black | Auto-formatter | Provides consistent, opinionated code formatting to help teams maintain a uniform coding style |
| Mypy | Static type checker | Type safety — catches type errors before runtime |
| Pyright | Static type checker | Faster alternative to Mypy, used by VS Code’s Pylance extension |
| Bandit | Security scanner | Identifies common security vulnerabilities in Python code |
Suggested configuration for a Python application using design patterns:
1 2 3 4 5 6 7 8 | bash pip install pylint flake8 black mypy bandit # Run all checks black . # Auto-format flake8 . # Style check mypy . --strict # Type check bandit -r . -ll # Security scan |
Such configuration will guarantee that each pattern implementation adheres to the same high standard, independent of who on the team implemented it.
Pattern Quick Reference
| Pattern | Category | Python native equivalent | When to use |
| Singleton | Creational | logging module, module-level variables | Shared resource with single access point |
| Factory | Creational | Functions returning instances | Object type determined at runtime |
| Abstract Factory | Creational | ABC + multiple concrete factories | Multiple related object families |
| Builder | Creational | Chained method calls, dataclasses | Complex objects with many optional params |
| Prototype | Creational | copy.deepcopy() | Expensive object creation |
| Adapter | Structural | Wrapper classes | Incompatible interface integration |
| Decorator | Structural | @decorator syntax | Runtime behaviour extension |
| Facade | Structural | Wrapper modules (e.g. requests) | Simplify complex subsystem access |
| Strategy | Behavioral | Callable injection, functools | Swappable algorithms at runtime |
| Observer | Behavioral | Django signals, asyncio events | Event-driven state notification |
Conclusion
Design patterns are guidelines rather than strict laws — they are solutions that have been tested repeatedly to solve problems. This essay has highlighted 10 patterns, which are the most practical ones when developing in Python: 5 Creational patterns to regulate object creation, 3 Structural patterns (Adapter, Decorator, and Facade) to compose and organize objects effectively, and 2 Behavioral patterns (Strategy and Observer) to handle interaction between objects.
The most crucial point to note about patterns is that their usage should be judged based on whether they address a challenge. Patterns should never be forced; they should be used only if there is an actual problem to solve. It’s best to keep things simple; sometimes a straightforward function or class will do.
If you want your Python application to be developed using design patterns at an architectural level, our Python developers at Innostax implement patterns in real-world projects daily — ranging from Django web apps to FastAPI microservices and ML pipelines.
