Implementation of Top Design Patterns in Python

Lisa Plitnichenko
Python Pandemonium
Published in
8 min readNov 8, 2020

*according to developer & software architect with 9+ years of experience

Design patterns in Python

In software development, design patterns are a proven solution to a common problem in a specific context.

Their main goal is to show us good ways to program things and explain why other options won’t work.

Using common design patterns, you can:

  • Speed up the development process;
  • Reduce the number of lines of code;
  • Make sure your code is well-designed;
  • Anticipate future problems arising from small issues.

Design patterns can considerably improve the life of a software developer disregarding the programming language (s)he uses.

I’ve interviewed the Founder & CTO of Jellyfish.tech, Python developer & software architect with 9+ years of experience Roman Latyshenko on his top design patterns.

I mainly work with Python/ Django, so here is my list of top patterns in Python I use daily in my job.

Behavioral patterns

Iterator

Iterator allows traversing the elements of collections without exposing the internal details.

Use case. Mostly, I use it to provide a standard way of traversing the collections.

âž• Clean client code (Single Responsibility Principle).

➕ Introducing iterators in collections is possible without changing the client’s code (Open/Closed Principle).

âž• Each iteration object has its own iteration state, so you can delay & continue iteration.

âž– Use of iterators with simple collections can overload the application.

Structure

Design pattern "Iterator" jellyfish.tech

Code example

from __future__ import annotations
from collections.abc import Iterable, Iterator
from typing import Any, List


class AlphabeticalOrderIterator(Iterator):
_position: int = None
_reverse: bool = False

def __init__(self, collection: WordsCollection,
reverse: bool = False):
self._collection = collection
self._reverse = reverse
self._position = -1 if reverse else 0

def __next__(self):
try:
value = self._collection[self._position]
self._position += -1 if self._reverse else 1
except IndexError:
raise StopIteration()
return value


class WordsCollection(Iterable):
def __init__(self, collection: List[Any] = []):
self._collection = collection

def __iter__(self) -> AlphabeticalOrderIterator:
return AlphabeticalOrderIterator(self._collection)

def get_reverse_iterator(self) -> AlphabeticalOrderIterator:
return AlphabeticalOrderIterator(self._collection, True)

def add_item(self, item: Any):
self._collection.append(item)


if __name__ == "__main__":
collection = WordsCollection()
collection.add_item("First")
collection.add_item("Second")
collection.add_item("Third")

print("Straight traversal:")
print("\n".join(collection))

print("Reverse traversal:")
print("\n".join(collection.get_reverse_iterator()))

State

State helps an object to alter its behavior in case its internal state changes.

Use case. State helps me

  • Alter the enormous number of object states.
  • Reduce the number of lines with duplicate code in similar transitions & states.
  • Avoid massive conditionals.

âž• Follows Single Responsibility principle: separate classes for the code related to a different state.

➕ Doesn’t change the context or state of classes when adding new states (Open/Closed Principle).

➖ Using State can be too much in case the state machine isn’t almost changing.

Structure

Design pattern "State" jellyfish.tech

Code example

from __future__ import annotations
from abc import ABC, abstractmethod
class Context(ABC):
_state = None
def __init__(self, state: State):
self.transition_to(state)
def transition_to(self, state: State):
print(f"Context: Transition to {type(state).__name__}")
self._state = state
self._state.context = self
def request1(self):
self._state.handle1()
def request2(self):
self._state.handle2()
class State(ABC):
@property
def context(self) -> Context:
return self._context
@context.setter
def context(self, context: Context):
self._context = context
@abstractmethod
def handle1(self):
pass
@abstractmethod
def handle2(self):
pass
class ConcreteStateA(State):
def handle1(self):
print("ConcreteStateA handles request1.")
print("ConcreteStateA wants to change the state of the context.")
self.context.transition_to(ConcreteStateB())
def handle2(self):
print("ConcreteStateA handles request2.")
class ConcreteStateB(State):
def handle1(self):
print("ConcreteStateB handles request1.")
def handle2(self):
print("ConcreteStateB handles request2.")
print("ConcreteStateB wants to change the state of the context.")
self.context.transition_to(ConcreteStateA())
if __name__ == "__main__":
context = Context(ConcreteStateA())
context.request1()
context.request2()

Observer

Observer notifies about events happening in other objects they observe without coupling to their classes.

Use case. Each time I need to add the subscription mechanism to let an object subscribe to/ unsubscribe from notifications on the events happening with a specific publisher class, I use the Observer pattern.

A good example is a simple subscription to news from any online magazine, frequently with the option to choose your sphere of interest (science, digital technology, etc.). Alternatively, the button “Notify me when it’s in stock” for e-commerce platforms is another example.

➕ You haven’t to change the publisher’s code to add subscribers’ classes.

âž– Subscribers get notifications in random order.

Structure

Design pattern "Observer" jellyfish.tech

Code example

from __future__ import annotations
from abc import ABC, abstractmethod
from random import randrange
from typing import List
class Subject(ABC):
@abstractmethod
def attach(self, observer: Observer):
pass
@abstractmethod
def detach(self, observer: Observer):
pass
@abstractmethod
def notify(self):
pass
class ConcreteSubject(Subject):
_state: int = None
_observers: List[Observer] = []

def attach(self, observer: Observer):
print("Subject: Attached an observer.")
self._observers.append(observer)
def detach(self, observer: Observer):
self._observers.remove(observer)
def notify(self):
print("Subject: Notifying observers...")
for observer in self._observers:
observer.update(self)
def some_business_logic(self):
print("Subject: I'm doing something important.")
self._state = randrange(0, 10)
print(f"Subject: My state has just changed to: {self._state}")
self.notify()
class Observer(ABC):
@abstractmethod
def update(self, subject: Subject):
pass
class ConcreteObserverA(Observer):
def update(self, subject: Subject):
if subject._state < 3:
print("ConcreteObserverA: Reacted to the event")
class ConcreteObserverB(Observer):
def update(self, subject: Subject):
if subject._state == 0 or subject._state >= 2:
print("ConcreteObserverB: Reacted to the event")
if __name__ == "__main__":
subject = ConcreteSubject()
observer_a = ConcreteObserverA()
subject.attach(observer_a)
observer_b = ConcreteObserverB()
subject.attach(observer_b)
subject.some_business_logic()
subject.some_business_logic()
subject.detach(observer_a) subject.some_business_logic()

Structural patterns

Facade

Facade provides a simplified yet limited interface to decrease the complexity of an application. Complex subsystems with multiple moving parts could be “masked” by Facade.

Use case. I create the Facade class in case I have to work with complex libraries & APIs and/ or I need only the part of their functionality.

âž• System complexity is separated from the code

âž– Using the Facade pattern, you can create a god object.

Structure

Design pattern "Facade" jellyfish.tech

Code example

class Addition:
def __init__(self, field1: int, field2: int):
self.field1 = field1
self.field2 = field2
def get_result(self):
return self.field1 + self.field2
class Multiplication:
def __init__(self, field1: int, field2: int):
self.field1 = field1
self.field2 = field2
def get_result(self):
return self.field1 * self.field2
class Subtraction:
def __init__(self, field1: int, field2: int):
self.field1 = field1
self.field2 = field2
def get_result(self):
return self.field1 - self.field2
class Facade:
@staticmethod
def make_addition(*args) -> Addition:
return Addition(*args)
@staticmethod
def make_multiplication(*args) -> Multiplication:
return Multiplication(*args)
@staticmethod
def make_subtraction(*args) -> Subtraction:
return Subtraction(*args)
if __name__ == "__main__":
addition_obj = Facade.make_addition(5, 5)
multiplication_obj = Facade.make_multiplication(5, 2)
subtraction_obj = Facade.make_subtraction(15, 5)
print(addition_obj.get_result())
print(multiplication_obj.get_result())
print(subtraction_obj.get_result())

Decorator

Decorator attaches new behaviors to the objects without modifying their structure.

The pattern produces a decorator class to wrap the original one and add new functionality.

Use case. I use the Decorator pattern each time I need to add extra behaviors to objects without getting into the code.

âž• Changes the object behavior without creating a subclass.

âž• You can combine several behaviors by wrapping an object into multiple decorators.

âž– A specific decorator is hard to remove from the wrappers stack.

Structure

Design pattern "Decorator" jellyfish.tech

Code example

class my_decorator:
def __init__(self, func):
print("inside my_decorator.__init__()")
func() # Prove that function definition has completed
def __call__(self):
print("inside my_decorator.__call__()")
@my_decorator
def my_function():
print("inside my_function()")
if __name__ == "__main__":
my_function()

Adapter

Adapter serves as the middle-layer class to join functionalities of either independent or incompatible interfaces.

Use case. Setting up the collaboration between the interfaces, I use the Adapter pattern to resolve the problem of incompatible formats.

For example, Adapter could help convert XML data format to JSON for further analysis.

âž• Allows separating the interface from business logic.

➕ Adding new adapters doesn’t break the client’s code

âž– Increases the code complexity

Structure

Design patter "Adapter" jellyfish.tech

Code example

class Target:
def request(self):
return "Target: The default target's behavior."
class Adaptee:
def specific_request(self):
return ".eetpadA eht fo roivaheb laicepS"
class Adapter(Target, Adaptee):
def request(self):
return f"Adapter: (TRANSLATED) {self.specific_request()[::-1]}"
def client_code(target: "Target"):
print(target.request())
if __name__ == "__main__":
print("Client: I can work just fine with the Target objects:")
target = Target()
client_code(target)
adaptee = Adaptee()

print("Client: The Adaptee class has a weird interface. "
"See, I don't understand it:")
print(f"Adaptee: {adaptee.specific_request()}")
print("Client: But I can work with it via the Adapter:")

adapter = Adapter()
client_code(adapter)

Creational patterns

Singleton

Singleton restricts a class from having more than one instance and ensures a global access point to this instance.

Use case. Singleton helps me

  • Manage a shared resource: i.e. a single database, file manager, or printer spooler shared by multiple parts of the application.
  • Store a global state (help filepath, user language, application path, etc.).
  • Create a simple logger.

âž• Class has a single instance

➖ It’s hard to unit test the code as the majority of test frameworks use inheritance when creating mock objects.

Structure

Design pattern "Singleton" jellyfish.tech

Code example

class Singleton:
def __new__(cls):
if not hasattr(cls, 'instance'):
cls.instance = super(Singleton, cls).__new__(cls)
return cls.instance
if __name__ == "__main__":
s = Singleton()
print("Object created:", s)
s1 = Singleton()
print("Object created:", s1)

When to use design patterns for Python?

Facade is good when you need a unified interface for several API options. I.e. you should integrate a payment system in the application, leaving the possibility to change it. In this case, you could use the Facade pattern, and you’ll only have to create a new facade without rewriting the whole application.

The problem here can emerge if only APIs are quite different, as it isn’t an easy task to design the common interface for facades.

State is used to manage the multiple independent components of an application provided that the initial architecture implies their independence. So, creating a separate module for state management can be a good idea, as well as using the Observer pattern.

Decorator is probably the most used Python pattern, because of in-built decorator support. For example, Decorator provides a convenient and explicit way to use some libraries and creates ever-richer opportunities for application design & management. The pattern also ensures a wide possibility for function composition and discovers new opportunities in functional programming.

Adapter is a fit when working with a big amount of data in different formats. The pattern allows using one algorithm instead of multiple ones for every data format.

The similar benefits Iterator has, so they are okay to be used together. In addition, one of the Iterator variations called Generator (introduced in Python long ago) allows using memory more efficiently working with big amounts of data that is highly valuable for some types of projects.

Finally, the importance of Singleton can’t be underestimated: database connection, working with API, working with files… These all are the moments when a developer should have a clear idea of how the process goes to avoid making mistakes. And Singleton can do a good job here, not to mention the possibility to reduce the memory consumption by using the same instance each time instead of duplicating it.

Got anything to add? Let's discuss it in the comments 🙌

--

--