For DevelopersOctober 01, 2024

Complete Guide to Python Design Patterns [Explanation+Codes]

Explore a comprehensive overview of Python design patterns, including Creational, Structural, and Behavioral types, with detailed explanations and code examples.

If you are coming across problems while designing and don’t know the way out, Python Design Patterns can be your saviour. It will not only answer complex questions but also make your codes maintainable. 

Python is an object-oriented language, so it is easier for you to write simple scripts, or you can just open the Python terminal and execute statements. Apart from this, you can also create complex applications, frameworks, and so much more. 

In this guide to Python Design Patterns, we will be covering the 3 types- Creational, Structural, and Behavioural along with their subtopics and codes. 

Find high-paying, long-term remote Python jobs with top US, UK, and EU companies at Index.dev. Join now!

 

Python design patterns

Introduction to Python Design Patterns

Design Patterns started to get attention after the book Design Patterns: Elements of Reusable Object-Oriented Software was published in 1994 by Erich Gamma, John Vlissides, Ralph Johnson, and Richard Helm. They are popularly known as the Gang of Four (GoF).

Design patterns are a typical approach to solving well-known issues. The GoF-defined design patterns are based on two major principles:

  • Program to an interface, not an implementation
  • Support object composition over inheritance

There are 3 types of Python Design Patterns that we will be covering in detail.

  • Creational Patterns
  • Structural Patterns
  • Behavioural Patterns

If you want to build a stronger foundation in core programming concepts before exploring advanced topics like design patterns, enrolling in a structured python language course can help you gain practical and hands-on experience.

Read Also: Understanding Data Types in Python Programming

 

Python creational patterns

Python Design Patterns #1: Creational Patterns

Creational Design Patterns are concerned with the production of classes and objects. They help to abstract away the intricacies of classes, allowing us to be less reliant on their precise implementation, avoid dealing with difficult creation everytime we require them, and assure some particular instantiation attributes.

The types of Creational Patterns are:

  • Factory Method
  • Abstract Factory
  • Singleton 
  • Builder
  • Prototype

Factory Method

The Factory Method Design Pattern is a creational design pattern implemented in software engineering to offer an interface for producing objects in a superclass while enabling subclasses to specify the sort of object to be generated.

It isolates the object creation logic into a separate method, abstracting the instantiation process and encouraging loose coupling between the creator and the generated objects. This pattern increases the codebase's flexibility, extensibility, and maintainability by letting subclasses design their own version of the factory function for creating various sorts of objects.

Here’s the Python code that shows the Factory Method pattern (example):

from abc import ABC, abstractmethod

# Product Interface
class Product(ABC):
    @abstractmethod
    def operation(self) -> str:
        pass

# Concrete Products
class ConcreteProductA(Product):
    def operation(self) -> str:
        return "Result of ConcreteProductA"

class ConcreteProductB(Product):
    def operation(self) -> str:
        return "Result of ConcreteProductB"

# Creator Interface
class Creator(ABC):
    @abstractmethod
    def factory_method(self) -> Product:
        pass

    def some_operation(self) -> str:
        # Call the factory method to create a Product object
        product = self.factory_method()
        # Use the product
        return f"Creator: {product.operation()}"

# Concrete Creators
class ConcreteCreatorA(Creator):
    def factory_method(self) -> Product:
        return ConcreteProductA()

class ConcreteCreatorB(Creator):
    def factory_method(self) -> Product:
        return ConcreteProductB()

# Client Code
def client_code(creator: Creator):
    print(creator.some_operation())

# Usage
creator_a = ConcreteCreatorA()
client_code(creator_a)  # Output: Creator: Result of ConcreteProductA

creator_b = ConcreteCreatorB()
client_code(creator_b)  # Output: Creator: Result of ConcreteProductB
  • Product Interface: Specifies the interface for the objects that the factory method will build. It defines an abstract method operation(), which concrete products must implement.
  • Concrete Products: Set up the product interface. ConcreteProductA and ConcreteProductB are two distinct implementations of the Product interface.
  • Creator Interface: Declares the factory method factory_method(), which yields a Product object. It also has a method some_operation(), which applies the factory method to obtain a Product instance and then performs some operation on it.
  • Concrete Creators: Implement the Creator interface and override the factory_method() to return a particular ConcreteProduct.
  • Client Code: Uses the Creator interface to communicate with Product objects. It doesn't need to know the precise class of the product it's dealing with, just that it follows the Product interface.

Abstract Factory

The Abstract Factory Pattern manages how you construct groupings of related objects. It gives a collection of rules or instructions that allow you to generate many sorts of objects without understanding what they are. This keeps everything structured and allows you to quickly transition between different kinds while adhering to the same set of rules.

The Abstract Factory interface has a number of creation methods that client applications may utilise to generate various sorts of UI components. Concrete factories correspond to certain operating systems and generate UI components for that OS.

It works as follows: when a program launches, it determines the kind of the current operating system. The app utilises this information to generate a factory object from a class that corresponds to the operating system. The remainder of the code employs this factory to generate UI components. This prevents incorrect items from being produced.

With this approach, the client code does not rely on concrete factory and UI element classes as long as it communicates with these objects through their abstract interfaces.

Here's an example of Python code:

// The abstract factory interface declares a set of methods that
// return different abstract products. These products are called
// a family and are related by a high-level theme or concept.
// Products of one family are usually able to collaborate among
// themselves. A family of products may have several variants,
// but the products of one variant are incompatible with the
// products of another variant.
interface GUIFactory is
    method createButton():Button
    method createCheckbox():Checkbox


// Concrete factories produce a family of products that belong
// to a single variant. The factory guarantees that the
// resulting products are compatible. Signatures of the concrete
// factory's methods return an abstract product, while inside
// the method a concrete product is instantiated.
class WinFactory implements GUIFactory is
    method createButton():Button is
        return new WinButton()
    method createCheckbox():Checkbox is
        return new WinCheckbox()

// Each concrete factory has a corresponding product variant.
class MacFactory implements GUIFactory is
    method createButton():Button is
        return new MacButton()
    method createCheckbox():Checkbox is
        return new MacCheckbox()


// Each distinct product of a product family should have a base
// interface. All variants of the product must implement this
// interface.
interface Button is
    method paint()

// Concrete products are created by corresponding concrete
// factories.
class WinButton implements Button is
    method paint() is
        // Render a button in Windows style.

class MacButton implements Button is
    method paint() is
        // Render a button in macOS style.

// Here's the base interface of another product. All products
// can interact with each other, but proper interaction is
// possible only between products of the same concrete variant.
interface Checkbox is
    method paint()

class WinCheckbox implements Checkbox is
    method paint() is
        // Render a checkbox in Windows style.

class MacCheckbox implements Checkbox is
    method paint() is
        // Render a checkbox in macOS style.


// The client code works with factories and products only
// through abstract types: GUIFactory, Button and Checkbox. This
// lets you pass any factory or product subclass to the client
// code without breaking it.
class Application is
    private field factory: GUIFactory
    private field button: Button
    constructor Application(factory: GUIFactory) is
        this.factory = factory
    method createUI() is
        this.button = factory.createButton()
    method paint() is
        button.paint()


// The application picks the factory type depending on the
// current configuration or environment settings and creates it
// at runtime (usually at the initialization stage).
class ApplicationConfigurator is
    method main() is
        config = readApplicationConfigFile()

        if (config.OS == "Windows") then
            factory = new WinFactory()
        else if (config.OS == "Mac") then
            factory = new MacFactory()
        else
            throw new Exception("Error! Unknown operating system.")

        Application app = new Application(factory)

Singleton 

The singleton pattern is one of Java's simplest design patterns. This design pattern is classified as a creational pattern since it gives one of the most effective methods for creating an item.

This pattern uses a single class that is responsible for creating an object while ensuring that only one object is generated. This class provides a method for accessing its sole object, which may be accessed directly without having to initialise the class's object.

Here are the steps to implement Singleton:

Step 1

Create a Singleton Class.

SingleObject.java

public class SingleObject {

   //create an object of SingleObject
   private static SingleObject instance = new SingleObject();

   //make the constructor private so that this class cannot be
   //instantiated
   private SingleObject(){}

   //Get the only object available
   public static SingleObject getInstance(){
      return instance;
   }

   public void showMessage(){
      System.out.println("Hello World!");
   }
}

Step 2

Get the only object from the singleton class.

SingletonPatternDemo.java

public class SingletonPatternDemo {
   public static void main(String[] args) {

      //illegal construct
      //Compile Time Error: The constructor SingleObject() is not visible
      //SingleObject object = new SingleObject();

      //Get the only object available
      SingleObject object = SingleObject.getInstance();

      //show the message
      object.showMessage();
   }
}

Step 3

Verify the output.

Also Read: 13 Python Algorithms Every Developer Should Know

 

Python structural patterns

Python Design Patterns #2: Structural Patterns

A structural design pattern is a blueprint for combining many objects and classes to produce a larger structure capable of fulfilling multiple roles. The patterns in structural designs demonstrate how different components of a system may be connected in an extendable and flexible manner. So, using the structural design pattern, we can focus and alter specific elements of the structure without affecting the overall system.

These patterns have two common themes:

  • This approach is very effective for getting independently built class libraries to operate together.
  • Structural design patterns outline how to combine things to create new functionality. The additional flexibility of object composition stems from the ability to modify the composition at runtime, which is not available with static class composition.

The types of Structural Patterns are:

  • Adapter 
  • Bridge 
  • Composite 
  • Decorator 
  • Facade 
  • Flyweight 
  • Proxy 

Adapter 

The Adapter design pattern is a structural pattern that enables the interface of one class to be employed as another. It serves as a bridge between two incompatible interfaces, allowing them to function together. This design uses a single class, the adapter, to combine the functions of separate or incompatible interfaces.

Let’s understand the code of the adapter design pattern using Python via example:

Imagine you have a Target interface that the client expects to use, but there is an Adaptee class that uses a different interface. The Adapter pattern enables the client to use the Adaptee via the Target interface.

# Target interface that the client expects to use
class Target:
    def request(self) -> str:
        return "Target: The default target's behavior."

# Adaptee class with an incompatible interface
class Adaptee:
    def specific_request(self) -> str:
        return ".eetpadA eht fo roivaheb cificeps"

# Adapter class that adapts the Adaptee's interface to the Target's interface
class Adapter(Target):
    def __init__(self, adaptee: Adaptee) -> None:
        self._adaptee = adaptee

    def request(self) -> str:
        # Adapt the specific_request to the request interface
        return f"Adapter: (TRANSLATED) {self._adaptee.specific_request()[::-1]}"

# Client code
def client_code(target: Target) -> None:
    print(target.request())

# Usage
print("Client: I can work just fine with the Target objects:")
target = Target()
client_code(target)

print("\nClient: The Adaptee class has a weird interface. See, I don't understand it:")
adaptee = Adaptee()
print(f"Adaptee: {adaptee.specific_request()}")

print("\nClient: But I can work with it via the Adapter:")
adapter = Adapter(adaptee)
client_code(adapter)

Here’s the explanation:

Target Interface

Target is the interface that the client intends to use. It contains a function called request().

Adaptee Class

Adaptee has an incompatible interface, with the function specific_request() returning a reversed string.

Adapter Class

Adapter derives from Target and contains an instance of Adaptee.

The request() function of the Adapter converts the specific_request() from the Adaptee to the Target interface by reversing the string to make it readable.

Client Code

The client can use Target directly, but when it wants to interact with Adaptee, it relies on the Adapter to convert the interface.

Here’s the output:

Client: I can work just fine with the Target objects:

Target: The default target's behaviour.

 

Client: The Adaptee class has a weird interface. See, I don't understand it:

Adaptee: .eetpadA eht fo roivaheb cificeps

 

Client: But I can work with it via the Adapter:

Adapter: (TRANSLATED) specific behaviour of the Adaptee.

  • Adapter: Converts the interface of a class into another interface that a client expects, allowing incompatible interfaces to work together.
  • Client Code: Remains unaware of the conversion and interacts with the Adapter as if it were the Target interface.

Bridge 

Bridge is used to separate an abstraction from its implementation, allowing the two to vary autonomously. This sort of design pattern is classified as a structural pattern since it decouples the implementation class from the abstract class by providing a bridge structure.

This design uses an interface as a bridge, allowing concrete classes to function independently of interface implementer classes. Both sorts of classes can be structurally changed without impacting the other.

Here’s the example code for the Bridge design pattern:

// Java code to demonstrate
// bridge design pattern

// abstraction in bridge pattern
abstract class Vehicle {
protected Workshop workShop1;
protected Workshop workShop2;

protected Vehicle(Workshop workShop1, Workshop workShop2)
{
this.workShop1 = workShop1;
this.workShop2 = workShop2;
}

abstract public void manufacture();
}

// Refine abstraction 1 in bridge pattern
class Car extends Vehicle {
public Car(Workshop workShop1, Workshop workShop2)
{
super(workShop1, workShop2);
}

@Override
public void manufacture()
{
System.out.print("Car ");
workShop1.work();
workShop2.work();
}
}

// Refine abstraction 2 in bridge pattern
class Bike extends Vehicle {
public Bike(Workshop workShop1, Workshop workShop2)
{
super(workShop1, workShop2);
}

@Override
public void manufacture()
{
System.out.print("Bike ");
workShop1.work();
workShop2.work();
}
}

// Implementer for bridge pattern
interface Workshop
{
abstract public void work();
}

// Concrete implementation 1 for bridge pattern
class Produce implements Workshop {
@Override
public void work()
{
System.out.print("Produced");
}
}

// Concrete implementation 2 for bridge pattern
class Assemble implements Workshop {
@Override
public void work()
{
System.out.print(" And");
System.out.println(" Assembled.");
}
}

// Demonstration of bridge design pattern
class BridgePattern {
public static void main(String[] args)
{
Vehicle vehicle1 = new Car(new Produce(), new Assemble());
vehicle1.manufacture();
Vehicle vehicle2 = new Bike(new Produce(), new Assemble());
vehicle2.manufacture();
}
}

Here’s the output:

  • Car Produced And Assembled
  • Bike Produced And Assembled

Composite 

The Composite Design Pattern is a structural design pattern that enables you to combine things into tree-like structures to depict partial-whole hierarchies. Clients can handle individual objects and object combinations uniformly. In other words, clients can use both a single object and a group of objects (composite) interchangeably.

Here are the elements:

  • Component: A component assists in providing the default behaviour for the interface shared by all classes, if appropriate. It defines the interface of the composition's objects, as well as how to access and manage its smaller components.
  • Leaf: It specifies the behaviour of primitive elements in the composition. It depicts the leaf in the composition.
  • Composite: It saves the child component and performs child-related activities within the component interface.
  • Client: It is used to manipulate the items in a composition via the component interface.

Here’s the example code for the Composite design pattern:

from __future__ import annotations
from abc import ABC, abstractmethod
from typing import List


class Component(ABC):
    """
    The base Component class declares common operations for both simple and
    complex objects of a composition.
    """

    @property
    def parent(self) -> Component:
        return self._parent

    @parent.setter
    def parent(self, parent: Component):
        """
        Optionally, the base Component can declare an interface for setting and
        accessing a parent of the component in a tree structure. It can also
        provide some default implementations for these methods.
        """

        self._parent = parent

    """
    In some cases, it would be beneficial to define the child-management
    operations right in the base Component class. This way, you won't need to
    expose any concrete component classes to the client code, even during the
    object tree assembly. The downside is that these methods will be empty for
    the leaf-level components.
    """

    def add(self, component: Component) -> None:
        pass

    def remove(self, component: Component) -> None:
        pass

    def is_composite(self) -> bool:
        """
        You can provide a method that lets the client code figure out whether a
        component can bear children.
        """

        return False

    @abstractmethod
    def operation(self) -> str:
        """
        The base Component may implement some default behavior or leave it to
        concrete classes (by declaring the method containing the behavior as
        "abstract").
        """

        pass


class Leaf(Component):
    """
    The Leaf class represents the end objects of a composition. A leaf can't
    have any children.

    Usually, it's the Leaf objects that do the actual work, whereas Composite
    objects only delegate to their sub-components.
    """

    def operation(self) -> str:
        return "Leaf"


class Composite(Component):
    """
    The Composite class represents the complex components that may have
    children. Usually, the Composite objects delegate the actual work to their
    children and then "sum-up" the result.
    """

    def __init__(self) -> None:
        self._children: List[Component] = []

    """
    A composite object can add or remove other components (both simple or
    complex) to or from its child list.
    """

    def add(self, component: Component) -> None:
        self._children.append(component)
        component.parent = self

    def remove(self, component: Component) -> None:
        self._children.remove(component)
        component.parent = None

    def is_composite(self) -> bool:
        return True

    def operation(self) -> str:
        """
        The Composite executes its primary logic in a particular way. It
        traverses recursively through all its children, collecting and summing
        their results. Since the composite's children pass these calls to their
        children and so forth, the whole object tree is traversed as a result.
        """

        results = []
        for child in self._children:
            results.append(child.operation())
        return f"Branch({'+'.join(results)})"


def client_code(component: Component) -> None:
    """
    The client code works with all of the components via the base interface.
    """

    print(f"RESULT: {component.operation()}", end="")


def client_code2(component1: Component, component2: Component) -> None:
    """
    Thanks to the fact that the child-management operations are declared in the
    base Component class, the client code can work with any component, simple or
    complex, without depending on their concrete classes.
    """

    if component1.is_composite():
        component1.add(component2)

    print(f"RESULT: {component1.operation()}", end="")


if __name__ == "__main__":
    # This way the client code can support the simple leaf components...
    simple = Leaf()
    print("Client: I've got a simple component:")
    client_code(simple)
    print("\n")

    # ...as well as the complex composites.
    tree = Composite()

    branch1 = Composite()
    branch1.add(Leaf())
    branch1.add(Leaf())

    branch2 = Composite()
    branch2.add(Leaf())

    tree.add(branch1)
    tree.add(branch2)

    print("Client: Now I've got a composite tree:")
    client_code(tree)
    print("\n")

    print("Client: I don't need to check the components classes even when managing the tree:")
    client_code2(tree, simple)

Here’s the output:

Client: I've got a simple component:

RESULT: Leaf

 

Client: Now I've got a composite tree:

RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf))

 

Client: I don't need to check the components classes even when managing the tree:

RESULT: Branch(Branch(Leaf+Leaf)+Branch(Leaf)+Leaf)

Also Read: How to Check the Type of Variable in Python

 

Python behavioural patterns

Python Design Patterns #3: Behavioural Patterns

Behavioral Patterns deal with algorithms and the distribution of responsibility among objects. Behavioural patterns identify not just patterns of items or classes, but also patterns of interaction between them. These patterns represent a sophisticated control flow that is difficult to understand at runtime.

These patterns consist of three repeating elements.

  • Behavioural class patterns employ inheritance to spread behaviour across classes.
  • Behavioural object patterns rely on object composition rather than inheritance.
  • Behavioural object patterns deal with encapsulating behaviour in an object and delegating queries to it.

The types of Behavioural Patterns are:

  • Chain of Responsibility
  • Command
  • Interpreter
  • Iterator
  • Mediator
  • Memento
  • Observer
  • State
  • Strategy
  • Template Method
  • Visitor

Chain of Responsibility

The Chain of Responsibility design pattern is a behavioural design pattern that enables an object to route a request via a series of handlers. Each handler in the chain determines whether to process the request or send it down the chain to the next handler. This design highlights a loose connection between sender and receiver, allowing for greater flexibility in processing requests.

Here’s an example of the Python code for Chain of Responsibility:

from abc import ABC, abstractmethod

# Handler Interface
class Handler(ABC):
    @abstractmethod
    def set_next(self, handler: 'Handler') -> 'Handler':
        pass

    @abstractmethod
    def handle(self, request: str) -> str:
        pass

# Base Handler
class AbstractHandler(Handler):
    _next_handler: Handler = None

    def set_next(self, handler: Handler) -> Handler:
        self._next_handler = handler
        return handler

    @abstractmethod
    def handle(self, request: str) -> str:
        if self._next_handler:
            return self._next_handler.handle(request)
        return None

# Concrete Handlers
class LowLevelApproval(AbstractHandler):
    def handle(self, request: str) -> str:
        if request == "LowLevel":
            return f"LowLevelApproval: Approved {request} request."
        else:
            return super().handle(request)

class MidLevelApproval(AbstractHandler):
    def handle(self, request: str) -> str:
        if request == "MidLevel":
            return f"MidLevelApproval: Approved {request} request."
        else:
            return super().handle(request)

class HighLevelApproval(AbstractHandler):
    def handle(self, request: str) -> str:
        if request == "HighLevel":
            return f"HighLevelApproval: Approved {request} request."
        else:
            return super().handle(request)

# Client code
def client_code(handler: Handler) -> None:
    requests = ["LowLevel", "MidLevel", "HighLevel", "Unknown"]
    for request in requests:
        result = handler.handle(request)
        if result:
            print(result)
        else:
            print(f"{request} request was not handled.")

# Usage
low_level = LowLevelApproval()
mid_level = MidLevelApproval()
high_level = HighLevelApproval()

low_level.set_next(mid_level).set_next(high_level)

print("Chain: LowLevel > MidLevel > HighLevel\n")
client_code(low_level)

Here’s the output:

Chain: LowLevel > MidLevel > HighLevel

 

LowLevelApproval: Approved LowLevel request.

MidLevelApproval: Approved MidLevel request.

HighLevelApproval: Approved HighLevel request.

Unknown request was not handled.

Command

The Command Method is a Behavioral Design Pattern that encapsulates a request as an object, allowing for client customization with different requests as well as request queuing or reporting. In our illustration, parameterizing additional objects with various demands implies that the button that turns on the lights may subsequently be used to turn on the radio or open the garage door. It aids in elevating the "invocation of a method on an object" to full object status. Essentially, it contains all of the information required to perform an action or initiate an event.

Here’s an example of the Python code for Command:

from __future__ import annotations
from abc import ABC, abstractmethod


class Command(ABC):
    """
    The Command interface declares a method for executing a command.
    """

    @abstractmethod
    def execute(self) -> None:
        pass


class SimpleCommand(Command):
    """
    Some commands can implement simple operations on their own.
    """

    def __init__(self, payload: str) -> None:
        self._payload = payload

    def execute(self) -> None:
        print(f"SimpleCommand: See, I can do simple things like printing"
              f"({self._payload})")


class ComplexCommand(Command):
    """
    However, some commands can delegate more complex operations to other
    objects, called "receivers."
    """

    def __init__(self, receiver: Receiver, a: str, b: str) -> None:
        """
        Complex commands can accept one or several receiver objects along with
        any context data via the constructor.
        """

        self._receiver = receiver
        self._a = a
        self._b = b

    def execute(self) -> None:
        """
        Commands can delegate to any methods of a receiver.
        """

        print("ComplexCommand: Complex stuff should be done by a receiver object", end="")
        self._receiver.do_something(self._a)
        self._receiver.do_something_else(self._b)


class Receiver:
    """
    The Receiver classes contain some important business logic. They know how to
    perform all kinds of operations, associated with carrying out a request. In
    fact, any class may serve as a Receiver.
    """

    def do_something(self, a: str) -> None:
        print(f"\nReceiver: Working on ({a}.)", end="")

    def do_something_else(self, b: str) -> None:
        print(f"\nReceiver: Also working on ({b}.)", end="")


class Invoker:
    """
    The Invoker is associated with one or several commands. It sends a request
    to the command.
    """

    _on_start = None
    _on_finish = None

    """
    Initialize commands.
    """

    def set_on_start(self, command: Command):
        self._on_start = command

    def set_on_finish(self, command: Command):
        self._on_finish = command

    def do_something_important(self) -> None:
        """
        The Invoker does not depend on concrete command or receiver classes. The
        Invoker passes a request to a receiver indirectly, by executing a
        command.
        """

        print("Invoker: Does anybody want something done before I begin?")
        if isinstance(self._on_start, Command):
            self._on_start.execute()

        print("Invoker: ...doing something really important...")

        print("Invoker: Does anybody want something done after I finish?")
        if isinstance(self._on_finish, Command):
            self._on_finish.execute()


if __name__ == "__main__":
    """
    The client code can parameterize an invoker with any commands.
    """

    invoker = Invoker()
    invoker.set_on_start(SimpleCommand("Say Hi!"))
    receiver = Receiver()
    invoker.set_on_finish(ComplexCommand(
        receiver, "Send email", "Save report"))

    invoker.do_something_important()

Here’s the output:

Invoker: Does anybody want something done before I begin?

SimpleCommand: See, I can do simple things like printing (Say Hi!)

Invoker: ...doing something really important...

Invoker: Does anybody want something done after I finish?

ComplexCommand: Complex stuff should be done by a receiver object

Receiver: Working on (Send email.)

Receiver: Also working on (Save report.)

Interpreter

The Interpreter Design Pattern assists with evaluating sentences in a language while also providing recommendations for translating phrases using a set of symbols. It is most efficient when defining language grammars and developing interpreters and parsers for such languages. It allows you to create an interpreter for a language using symbols (or expressions). You must specify a class for each symbol (or expression) and implement an interpret function to evaluate these symbols.

Let’s have a look at the code example of Interpreter design pattern, using a simple language to interpret and evaluate arithmetic expressions with variables and basic operations. In this example, we'll define an interpreter for evaluating expressions like a * (b + c) - d.

from abc import ABC, abstractmethod

# Abstract Expression
class Expression(ABC):
    @abstractmethod
    def interpret(self, context: dict) -> int:
        pass

# Terminal Expression
class Variable(Expression):
    def __init__(self, name: str) -> None:
        self._name = name

    def interpret(self, context: dict) -> int:
        return context.get(self._name, 0)

# Non-terminal Expressions
class Addition(Expression):
    def __init__(self, left: Expression, right: Expression) -> None:
        self._left = left
        self._right = right

    def interpret(self, context: dict) -> int:
        return self._left.interpret(context) + self._right.interpret(context)

class Subtraction(Expression):
    def __init__(self, left: Expression, right: Expression) -> None:
        self._left = left
        self._right = right

    def interpret(self, context: dict) -> int:
        return self._left.interpret(context) - self._right.interpret(context)

class Multiplication(Expression):
    def __init__(self, left: Expression, right: Expression) -> None:
        self._left = left
        self._right = right

    def interpret(self, context: dict) -> int:
        return self._left.interpret(context) * self._right.interpret(context)

class Parentheses(Expression):
    def __init__(self, expression: Expression) -> None:
        self._expression = expression

    def interpret(self, context: dict) -> int:
        return self._expression.interpret(context)

# Client Code
def client_code(expression: Expression, context: dict) -> None:
    result = expression.interpret(context)
    print(f"Result: {result}")

# Usage
# a * (b + c) - d
expression = Subtraction(
    Multiplication(
        Variable("a"),
        Parentheses(
            Addition(Variable("b"), Variable("c"))
        )
    ),
    Variable("d")
)

context = {
    "a": 5,
    "b": 3,
    "c": 2,
    "d": 4
}

client_code(expression, context)

The Output:

Result: 19

Read Also: What Is Multiprocessing in Python

 

Conclusion

Understanding these patterns enables developers to handle common design issues with established solutions, resulting in more resilient and flexible software structures.

  • Creational patterns make object creation easier and assure consistency across several components. These patterns enable the flexible and controlled creation of objects, adapting to various requirements without changing the client code.
  • Structural patterns assist in managing and enhancing object interactions, ensuring that they perform seamlessly together. These patterns address issues with object composition and structure, encouraging code reuse and lowering complexity.
  • Behavioural patterns deal with how things interact and communicate. These patterns simplify communication and control processes, making handling complicated workflows and responsibilities easier.

 

For Python Developers:

Join Index.dev’s community of developers to take your Python career to the next level!

For Clients: 

Looking for senior Python developers? Index.dev connects you with the most thoroughly vetted ones who are ready to work on your most challenging projects. Hire in under 48 hours!

Share

Laveena HoraLaveena HoraAuthor

Related Articles

For Developers4 Easy Ways to Check for NaN Values in Python
Use np.isnan() in NumPy to check NaN in arrays. In Pandas, use isna() or isnull() for DataFrames. For single float values, use math.isnan(). If you're working with Decimal, use is_nan() from the decimal module. Each method fits different data types and use cases.
Ali MojaharAli MojaharSEO Specialist
For Developers13 Python Algorithms Every Developer Should Know
Dive into 13 fundamental Python algorithms, explaining their importance, functionality, and implementation.
Radu PoclitariRadu PoclitariCopywriter