Design Patterns in Python and Their Use in Frameworks

Master Spring Ter
9 min readOct 13, 2024

Python, known for its simplicity and versatility, provides excellent support for implementing various design patterns. These patterns offer reusable solutions to common programming challenges, helping developers build scalable, maintainable, and efficient applications. Python and popular frameworks like Django, Flask, and SQLAlchemy incorporate many design patterns into their architecture, offering developers powerful tools without needing to implement these solutions from scratch. This article explores several key design patterns that are built into Python and its frameworks, illustrating their importance and applications. Each pattern discussed includes examples and explanations to help you understand its relevance and effective usage.

Creational Design Patterns in Python and Their Use in Frameworks
Structural Design Patterns in Python and Their Use in Frameworks
Behavioral Design Patterns in Python and Their Use in Frameworks

Unlock the power of free AI-assisted development with ChatGPT models! For Java developers, explore advanced features and support with Mastering Spring with ChatGPT. If you’re a Python enthusiast, elevate your coding with ChatGPT’s Python Assistant. Enhance productivity, debug efficiently, and streamline your projects with AI-driven insights — all for free!

1. Singleton Pattern

Pattern Overview:
The Singleton pattern ensures that only one instance of a class exists throughout an application. It is useful when you need a single point of access to a resource, such as database connections or loggers. Singleton is particularly beneficial in scenarios where creating multiple instances of an object is resource-intensive or can lead to inconsistent behavior.

Python Example:
Python’s logging module uses the Singleton pattern. The getLogger() method ensures that every time a logger with the same name is called, the same logger instance is returned, centralizing logging across the application.

import logging

logger1 = logging.getLogger('app_logger')
logger2 = logging.getLogger('app_logger')
print(logger1 is logger2) # True

This guarantees that the log messages are consistently handled, making debugging and monitoring easier.

Framework Usage:
Django, in its settings management, also employs Singleton-like behavior by ensuring that the same configuration is used throughout the application lifecycle. This approach ensures consistency across different parts of the Django application, where all components rely on the same settings.

Another example is in the caching mechanisms used by Django, where a Singleton-like pattern is applied to ensure that only one cache instance is used, avoiding redundant resource allocation.

2. Factory Method Pattern

Pattern Overview:
The Factory Method pattern delegates the responsibility of object creation to subclasses, enabling greater flexibility in choosing which object to create at runtime. This is especially useful when the exact type of the object being created is determined by user input or program conditions.

Python Example:
In core Python, the unittest framework uses the Factory Method pattern. The TestLoader class is responsible for discovering and creating instances of test cases, encapsulating the logic behind object creation.

import unittest

class MyTestCase(unittest.TestCase):
def test_something(self):
self.assertEqual(True, True)
loader = unittest.TestLoader()
suite = loader.loadTestsFromTestCase(MyTestCase)

The TestLoader abstracts the creation process, allowing the developer to focus on writing tests rather than managing their instantiation.

Framework Usage:
Django uses the Factory Method pattern extensively, particularly in its ORM (Object-Relational Mapping) system. When creating models, developers don’t need to instantiate objects manually; instead, Django’s ORM uses factories to create the appropriate model instances based on provided parameters.

# Example in Django ORM
user = User.objects.create(username="john_doe")

Here, User.objects.create() is a Factory Method that abstracts away the process of object creation, making the code cleaner and more maintainable.

In Flask, a similar pattern is used for handling different types of requests. Flask allows for dynamic route creation using factories that determine the appropriate view function to handle specific types of HTTP requests.

3. Observer Pattern

Pattern Overview:
The Observer pattern defines a one-to-many relationship between objects, where the subject notifies all its dependents (observers) whenever it changes its state. This pattern is ideal for implementing event-driven systems, where changes in one part of the system should automatically propagate to others.

Python Example:
In core Python, the Observable class in third-party libraries (like RxPy) implements the Observer pattern. This approach allows for the reactive programming style, where events drive the flow of the program.

from rx import Observable

def push_numbers(observer, _):
for i in range(5):
observer.on_next(i)
observer.on_completed()
observable = Observable.create(push_numbers)
observable.subscribe(lambda x: print(f'Received: {x}'))

In this example, subscribers are notified whenever there is a new value to process.

Framework Usage:
Django’s Signals framework is a classic implementation of the Observer pattern. It allows specific senders to notify listeners when certain events occur, such as when a model is saved or a user logs in.

from django.db.models.signals import post_save
from django.dispatch import receiver
from myapp.models import UserProfile

@receiver(post_save, sender=UserProfile)
def post_save_user_profile(sender, instance, **kwargs):
print(f"UserProfile for {instance.user} was saved!")

Whenever a UserProfile is saved, the attached listeners are notified and perform the appropriate actions. This decouples the code, making it more modular and easier to maintain.

4. Decorator Pattern

Pattern Overview:
The Decorator pattern allows behavior to be added to individual objects dynamically without modifying the class itself. This makes it particularly useful for adhering to the Open/Closed Principle — open for extension but closed for modification.

Python Example:
Python’s built-in functools module supports decorators that can modify functions or methods at runtime.

import functools

def my_decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
print("Before function call")
result = func(*args, **kwargs)
print("After function call")
return result
return wrapper
@my_decorator
def say_hello():
print("Hello!")
say_hello()

Decorators are powerful because they allow you to add logging, authentication, or caching functionalities without changing the core logic of a function.

Framework Usage:
In Flask, decorators are used extensively for route handling:

@app.route('/')
def index():
return "Hello, World!"

Here, the @app.route decorator dynamically adds behavior to the index() function, associating it with a URL endpoint. This approach makes it easier to manage routes and their associated functionalities.

In Django, decorators such as @login_required are used to enforce authentication for views, adding security without altering the view’s core logic.

5. Strategy Pattern

Pattern Overview:
The Strategy pattern enables selecting different algorithms at runtime. This is useful when different algorithms are interchangeable and can be swapped out dynamically, depending on user needs or system conditions.

Python Example:
The sorted() function in Python is an example of the Strategy pattern, as it allows passing different sorting strategies using the key argument.

items = ['apple', 'banana', 'cherry']
sorted_items = sorted(items, key=len)
print(sorted_items) # Output: ['apple', 'banana', 'cherry']

By providing different functions to the key argument, developers can dynamically change the sorting behavior.

Framework Usage:
In Django, the Strategy pattern is evident in form validation, where different validation strategies can be applied to different fields. Django’s form fields can be customized with various validation functions, allowing developers to enforce different business rules without modifying the base form logic.

6. Adapter Pattern

Pattern Overview:
The Adapter pattern allows incompatible interfaces to work together by adapting one interface to another. This is particularly useful when integrating third-party code, as it allows developers to bridge the gap between different APIs or systems.

Python Example:
Python’s subprocess module acts as an adapter, translating system-level commands to Python-friendly interfaces.

import subprocess

result = subprocess.run(['ls', '-l'], capture_output=True, text=True)
print(result.stdout)

The subprocess module provides a consistent interface for executing system commands, regardless of the underlying operating system.

Framework Usage:
In SQLAlchemy, the Adapter pattern is used to provide a consistent interface to different database backends, allowing developers to switch between databases without changing their code. This makes SQLAlchemy highly flexible, as it can seamlessly adapt to different database engines like SQLite, PostgreSQL, or MySQL.

7. Proxy Pattern

Pattern Overview:
The Proxy pattern provides a surrogate or placeholder for another object to control access to it. This is useful for scenarios like lazy initialization, access control, or logging. Proxies act as intermediaries that add additional behavior without changing the underlying object.

Python Example:
Python’s property function can act as a Proxy by controlling access to instance attributes.

class Product:
def __init__(self, price):
self._price = price

@property
def price(self):
print("Getting price")
return self._price
product = Product(50)
print(product.price) # Getting price

The @property decorator allows for controlled access, making it possible to add logging or validation when an attribute is accessed.

Framework Usage:
In Django, the @cached_property decorator is used to cache results of a method, acting as a proxy for computed properties, thereby enhancing performance by controlling resource access. This helps in avoiding repetitive calculations, making the application more efficient.

8. Command Pattern

Pattern Overview:
The Command pattern encapsulates a request as an object, thereby allowing parameterization of clients with different requests, queuing of requests, and logging the requests. It is particularly useful for implementing undoable operations.

Python Example:
In Python, commands can be implemented as objects that encapsulate actions, such as button clicks.

class Command:
def execute(self):
pass

class PrintCommand(Command):
def __init__(self, message):
self.message = message
def execute(self):
print(self.message)
command = PrintCommand("Hello, Command Pattern!")
command.execute()

Framework Usage:
In Django, the Command pattern is used in management commands, such as custom commands for managing data or running scheduled tasks. This allows encapsulating the functionality into reusable command objects that can be triggered via the command line.

9. Builder Pattern

Pattern Overview:
The Builder pattern separates the construction of a complex object from its representation, enabling the same construction process to create different representations. This is useful for creating objects that require many configuration steps.

Python Example:
In Python, list comprehensions can be thought of as a simple example of the Builder pattern.

squares = [x * x for x in range(10)]
print(squares) # Output: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Framework Usage:
SQLAlchemy makes use of the Builder pattern when creating complex database queries, enabling the developer to build queries in a step-by-step manner.

10. Composite Pattern

Pattern Overview:
The Composite pattern is used to compose objects into tree structures to represent part-whole hierarchies. It allows clients to treat individual objects and compositions of objects uniformly.

Python Example:
The os.path module can be used as a simple example of the Composite pattern, allowing operations on both individual files and directories.

Framework Usage:
In Django, form field composition is an example of the Composite pattern. Forms can contain individual fields as well as groups of fields, and they can be managed in a uniform way.

11. Iterator Pattern

Pattern Overview:
The Iterator pattern provides a way to access elements of a collection sequentially without exposing the underlying representation. It is useful for traversing data structures like lists, trees, or graphs.

Python Example:
Python’s iter() and next() functions are built-in tools for implementing iterators.

my_list = [1, 2, 3]
iterator = iter(my_list)
print(next(iterator)) # Output: 1
print(next(iterator)) # Output: 2

Framework Usage:
In Django, QuerySets implement the Iterator pattern, allowing efficient traversal of database query results without loading all records into memory at once.

12. Chain of Responsibility Pattern

Pattern Overview:
The Chain of Responsibility pattern passes a request along a chain of potential handlers until one of them handles it. This pattern is useful for reducing the coupling between sender and receiver.

Python Example:
Middleware in web applications is an example of the Chain of Responsibility pattern, where each piece of middleware processes a request in turn.

Framework Usage:
In Django, middleware components are chained together to process HTTP requests and responses. Each middleware in the chain has the opportunity to process the request before passing it to the next component.

Conclusion

Design patterns are essential tools in a developer’s toolkit, helping solve common software design challenges. Python, with its flexibility, incorporates these patterns both in its core libraries and in popular frameworks like Django, Flask, and SQLAlchemy. From the Singleton pattern in the logging module to the Observer pattern in Django’s signals, understanding these patterns enhances the ability to write clean, maintainable, and scalable code.

By using these pre-established patterns, developers can focus more on solving business problems rather than reinventing the wheel. Understanding and applying these patterns not only improves code quality but also fosters better communication among developers by providing a common language for addressing design challenges. Whether you’re building a small web application or a large-scale system, leveraging design patterns will help ensure that your code remains robust, flexible, and easy to understand as your project grows.

Incorporating design patterns into your development practices enables you to anticipate future needs and accommodate changes more gracefully. This foresight helps in managing technical debt and maintaining code that remains relevant and effective, regardless of the evolving requirements. Design patterns are not just theoretical constructs — they are practical solutions that empower developers to create high-quality software that stands the test of time.

generated by https://chatgpt.com/g/g-AsIZLxXd2-python

Sign up to discover human stories that deepen your understanding of the world.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Master Spring Ter
Master Spring Ter

Written by Master Spring Ter

https://chatgpt.com/g/g-dHq8Bxx92-master-spring-ter Specialized ChatGPT expert in Spring Boot, offering insights and guidance for developers.

No responses yet

Write a response