Design Patterns in Python (Factory, Observer, Singleton, and More)


Table of Contents

  • Introduction
  • What are Design Patterns?
  • Why Use Design Patterns in Python?
  • Categories of Design Patterns
  • Creational Patterns
    • Factory Pattern
    • Singleton Pattern
  • Structural Patterns
    • Adapter Pattern
    • Decorator Pattern
  • Behavioral Patterns
    • Observer Pattern
    • Strategy Pattern
  • Choosing the Right Pattern
  • Best Practices When Implementing Patterns
  • Conclusion

Introduction

As software projects grow in complexity, ensuring that your codebase remains organized, reusable, and easy to maintain becomes critical.
Design patterns provide proven solutions to recurring design problems and help developers write better, more scalable Python code.
In this article, we will explore some of the most commonly used design patterns such as Factory, Observer, Singleton, and others — all deeply explained and implemented with practical examples.


What are Design Patterns?

A design pattern is a reusable solution to a common software design problem.
It is not a finished design or ready-made code but a guideline or template that can be adapted to solve problems within specific contexts.

Design patterns became widely popular after the publication of the book “Design Patterns: Elements of Reusable Object-Oriented Software” by the “Gang of Four” (GoF).


Why Use Design Patterns in Python?

  • Faster Development: Reuse time-tested solutions.
  • Better Communication: Developers can quickly understand code when standard patterns are used.
  • Maintainability: Easier to extend or modify the application.
  • Scalability: Patterns help create scalable system architectures.

Even though Python has dynamic typing and multiple paradigms (procedural, object-oriented, functional), applying design patterns correctly can greatly enhance code quality.


Categories of Design Patterns

Design patterns are generally categorized into three main types:

  • Creational: Deal with object creation mechanisms (e.g., Factory, Singleton).
  • Structural: Deal with object composition and structure (e.g., Adapter, Decorator).
  • Behavioral: Deal with object collaboration and responsibility (e.g., Observer, Strategy).

Creational Patterns

Factory Pattern

Purpose:
Creates objects without exposing the instantiation logic to the client and refers to the newly created object using a common interface.

Example:

class Dog:
def speak(self):
return "Woof!"

class Cat:
def speak(self):
return "Meow!"

class AnimalFactory:
@staticmethod
def create_animal(animal_type):
if animal_type == "Dog":
return Dog()
elif animal_type == "Cat":
return Cat()
else:
raise ValueError("Unknown Animal Type")

# Client code
animal = AnimalFactory.create_animal("Dog")
print(animal.speak())

Use cases:

  • When the exact type of object needs to be determined at runtime.

Singleton Pattern

Purpose:
Ensures that a class has only one instance and provides a global point of access to it.

Example:

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 Database(metaclass=SingletonMeta):
pass

db1 = Database()
db2 = Database()

print(db1 is db2) # Output: True

Use cases:

  • Managing database connections.
  • Configuration management.

Structural Patterns

Adapter Pattern

Purpose:
Allows incompatible interfaces to work together.

Example:

class EuropeanSocketInterface:
def connect(self):
pass

class EuropeanSocket(EuropeanSocketInterface):
def connect(self):
return "Connected to European socket."

class AmericanSocket:
def plug_in(self):
return "Connected to American socket."

class SocketAdapter(EuropeanSocketInterface):
def __init__(self, american_socket):
self.american_socket = american_socket

def connect(self):
return self.american_socket.plug_in()

# Client code
european_socket = EuropeanSocket()
american_socket = AmericanSocket()
adapter = SocketAdapter(american_socket)

print(adapter.connect())

Decorator Pattern

Purpose:
Adds behavior to objects dynamically without altering their structure.

Example:

def make_bold(func):
def wrapper():
return "<b>" + func() + "</b>"
return wrapper

def make_italic(func):
def wrapper():
return "<i>" + func() + "</i>"
return wrapper

@make_bold
@make_italic
def hello():
return "Hello, World!"

print(hello())

Behavioral Patterns

Observer Pattern

Purpose:
Allows an object (subject) to notify other objects (observers) about changes in its state.

Example:

class Subject:
def __init__(self):
self._observers = []

def register(self, observer):
self._observers.append(observer)

def notify_all(self, message):
for observer in self._observers:
observer.update(message)

class Observer:
def update(self, message):
print(f"Observer received message: {message}")

# Usage
subject = Subject()
observer1 = Observer()
observer2 = Observer()

subject.register(observer1)
subject.register(observer2)

subject.notify_all("Event happened!")

Use cases:

  • Implementing publish/subscribe systems.
  • UI event handling.

Strategy Pattern

Purpose:
Enables selecting an algorithm’s behavior at runtime.

Example:

class QuickSort:
def sort(self, data):
return sorted(data)

class BubbleSort:
def sort(self, data):
# Bubble sort implementation
for i in range(len(data)):
for j in range(0, len(data)-i-1):
if data[j] > data[j+1]:
data[j], data[j+1] = data[j+1], data[j]
return data

class Sorter:
def __init__(self, strategy):
self.strategy = strategy

def sort(self, data):
return self.strategy.sort(data)

data = [5, 2, 9, 1]
sorter = Sorter(QuickSort())
print(sorter.sort(data)) # Output: [1, 2, 5, 9]

Choosing the Right Pattern

There is no universal rule for choosing a design pattern.
However, understanding the purpose of each pattern and the problem it solves will guide you:

  • Factory: When object creation needs to be abstracted.
  • Singleton: When exactly one instance is needed.
  • Adapter: When you want two incompatible interfaces to work together.
  • Observer: When an object should notify multiple objects of changes.
  • Strategy: When you need multiple interchangeable algorithms.

Best Practices When Implementing Patterns

  • Do not overuse patterns: Only use them when the problem demands it.
  • Prefer composition over inheritance: Most patterns encourage composition.
  • Keep patterns simple and clear: Avoid unnecessary complexity.
  • Use Pythonic features: Leverage dynamic typing, decorators, and first-class functions to implement patterns cleanly.

Conclusion

Mastering design patterns equips you with the tools to tackle complex problems using established, battle-tested approaches.
By incorporating patterns like Factory, Observer, Singleton, and others into your Python projects, you can write code that is easier to understand, maintain, and extend.
With experience, selecting the appropriate pattern for a problem will become an intuitive part of your software design process, elevating you to a more advanced and professional level of Python development.

Syskoolhttps://syskool.com/
Articles are written and edited by the Syskool Staffs.