Skip to content

Creational Design Patterns in Python

What Are Creational Design Patterns?

Creational design patterns abstract the instantiation process, making systems independent of how objects are created, composed, and represented. They help manage complexity when creating objects and make your code more maintainable.

The Five Creational Patterns

1. Singleton Pattern

The Singleton pattern ensures a class has only one instance and provides a global point of access to it.

Use Case: Database connections, logging, configuration managers

class DatabaseConnection:
    _instance = None

    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
            cls._instance.connection = None
        return cls._instance

    def connect(self, connection_string):
        if not self.connection:
            self.connection = connection_string
            print(f"Connected to: {connection_string}")
        return self.connection

# Usage
db1 = DatabaseConnection()
db1.connect("postgresql://localhost:5432/mydb")

db2 = DatabaseConnection()
print(db1 is db2)  # True - same instance

2. Factory Method Pattern

Factory Method defines an interface for creating objects but lets subclasses decide which class to instantiate.

Use Case: Document generators, UI component creation, payment processors

from abc import ABC, abstractmethod

class PaymentProcessor(ABC):
    @abstractmethod
    def process_payment(self, amount):
        pass

class CreditCardProcessor(PaymentProcessor):
    def process_payment(self, amount):
        return f"Processing ${amount} via Credit Card"

class PayPalProcessor(PaymentProcessor):
    def process_payment(self, amount):
        return f"Processing ${amount} via PayPal"

class PaymentFactory:
    @staticmethod
    def create_processor(payment_type):
        if payment_type == "credit_card":
            return CreditCardProcessor()
        elif payment_type == "paypal":
            return PayPalProcessor()
        else:
            raise ValueError(f"Unknown payment type: {payment_type}")

# Usage
processor = PaymentFactory.create_processor("paypal")
print(processor.process_payment(100))

3. Abstract Factory Pattern

Abstract Factory provides an interface for creating families of related objects without specifying their concrete classes.

Use Case: Cross-platform UI toolkits, theme systems

from abc import ABC, abstractmethod

# Abstract Products
class Button(ABC):
    @abstractmethod
    def render(self):
        pass

class Checkbox(ABC):
    @abstractmethod
    def render(self):
        pass

# Concrete Products - Windows
class WindowsButton(Button):
    def render(self):
        return "Rendering Windows Button"

class WindowsCheckbox(Checkbox):
    def render(self):
        return "Rendering Windows Checkbox"

# Concrete Products - Mac
class MacButton(Button):
    def render(self):
        return "Rendering Mac Button"

class MacCheckbox(Checkbox):
    def render(self):
        return "Rendering Mac Checkbox"

# Abstract Factory
class GUIFactory(ABC):
    @abstractmethod
    def create_button(self) -> Button:
        pass

    @abstractmethod
    def create_checkbox(self) -> Checkbox:
        pass

# Concrete Factories
class WindowsFactory(GUIFactory):
    def create_button(self):
        return WindowsButton()

    def create_checkbox(self):
        return WindowsCheckbox()

class MacFactory(GUIFactory):
    def create_button(self):
        return MacButton()

    def create_checkbox(self):
        return MacCheckbox()

# Usage
def render_ui(factory: GUIFactory):
    button = factory.create_button()
    checkbox = factory.create_checkbox()
    print(button.render())
    print(checkbox.render())

render_ui(MacFactory())

4. Builder Pattern

Builder separates the construction of complex objects from their representation, allowing the same construction process to create different representations.

Use Case: Building complex objects like HTTP requests, SQL queries, or configuration objects

class Pizza:
    def __init__(self):
        self.size = None
        self.crust = None
        self.toppings = []

    def __str__(self):
        return f"{self.size} pizza with {self.crust} crust and toppings: {', '.join(self.toppings)}"

class PizzaBuilder:
    def __init__(self):
        self.pizza = Pizza()

    def set_size(self, size):
        self.pizza.size = size
        return self

    def set_crust(self, crust):
        self.pizza.crust = crust
        return self

    def add_topping(self, topping):
        self.pizza.toppings.append(topping)
        return self

    def build(self):
        return self.pizza

# Usage
pizza = (PizzaBuilder()
         .set_size("Large")
         .set_crust("Thin")
         .add_topping("Pepperoni")
         .add_topping("Mushrooms")
         .add_topping("Extra Cheese")
         .build())

print(pizza)

5. Prototype Pattern

Prototype creates new objects by copying existing objects (prototypes) rather than creating them from scratch.

Use Case: Cloning complex objects, avoiding expensive initialization

import copy

class GameCharacter:
    def __init__(self, name, level, equipment):
        self.name = name
        self.level = level
        self.equipment = equipment  # List of items

    def clone(self):
        # Deep copy to avoid sharing mutable objects
        return copy.deepcopy(self)

    def __str__(self):
        return f"{self.name} (Level {self.level}) - Equipment: {self.equipment}"

# Usage
original_warrior = GameCharacter("Warrior", 50, ["Sword", "Shield", "Armor"])
print(f"Original: {original_warrior}")

# Clone and modify
cloned_warrior = original_warrior.clone()
cloned_warrior.name = "Shadow Warrior"
cloned_warrior.equipment.append("Magic Ring")

print(f"Clone: {cloned_warrior}")
print(f"Original after cloning: {original_warrior}")  # Original unchanged

When to Use Each Pattern

Pattern Use When
Singleton You need exactly one instance (logging, config)
Factory Method You need flexibility in object creation
Abstract Factory You need to create families of related objects
Builder You need to construct complex objects step-by-step
Prototype Copying is cheaper than creating from scratch

Python-Specific Considerations

  1. Singleton: Python's module system naturally implements the Singleton pattern—modules are singletons by default.
  2. Factory Method: Python's duck typing makes factories even more flexible than in statically-typed languages.
  3. Builder: Method chaining (fluent interface) works beautifully in Python for the Builder pattern.
  4. Prototype: Python's copy module makes implementing prototypes straightforward.

Best Practices

  • Don't overuse patterns: Apply them when they solve real problems, not for the sake of using patterns.
  • Keep it Pythonic: Python's dynamic nature often provides simpler alternatives to traditional patterns.
  • Test thoroughly: Patterns add abstraction layers—ensure they're well-tested.
  • Document your intent: Make it clear why you're using a particular pattern.

Conclusion

Creational design patterns are powerful tools in a Python developer's toolkit. They promote loose coupling, improve code organization, and make systems more maintainable. Start by identifying repetitive object creation logic in your codebase and consider which pattern might help.