Loading episodes…
0:00 0:00

Clone, Don't Recreate: A Visual Guide to the Prototype Pattern in Python

00:00
BACK TO HOME

Clone, Don't Recreate: A Visual Guide to the Prototype Pattern in Python

10xTeam November 05, 2025 12 min read

Have you ever found yourself stuck in a loop, creating objects that are almost identical but require complex, multi-step initialization? You write long if/else blocks or complex constructors, and every time a new variation is needed, the logic grows more tangled. This approach is not only tedious but also brittle and hard to maintain.

What if you could create a “master copy” of an object and then simply clone it whenever you need a new instance?

This is the core idea behind the Prototype design pattern. It allows you to create new objects by copying a pre-configured instance, known as a prototype, rather than creating them from scratch.

The Problem: Repetitive and Inflexible Object Creation

Imagine a system that generates invoices. You might have several types of invoices: one for monthly subscriptions, one for hosting services, and another for custom development work. While they share common elements like a header, footer, and tax rate, they differ in their line items and descriptions.

A naive implementation might look like this: a single function with a large conditional block that builds the invoice based on its type.

# The "Before" Scenario: A rigid and repetitive function
def create_invoice(customer_type, customer_name, invoice_number):
    if customer_type == "subscription":
        # Repetitive initialization
        invoice = {
            "header": "Default Company Header",
            "footer": "Default Company Footer",
            "tax_rate": 0.15,
            "items": [{"description": "Monthly Subscription", "price": 1000}],
            "customer_name": customer_name,
            "invoice_number": invoice_number,
        }
        return invoice
    elif customer_type == "hosting":
        # More repetition
        invoice = {
            "header": "Default Company Header",
            "footer": "Default Company Footer",
            "tax_rate": 0.15,
            "items": [{"description": "Hosting Service", "price": 1500}],
            "customer_name": customer_name,
            "invoice_number": invoice_number,
        }
        return invoice
    elif customer_type == "custom_development":
        # And again...
        invoice = {
            "header": "Default Company Header",
            "footer": "Default Company Footer",
            "tax_rate": 0.18, # A different tax rate complicates things
            "items": [{"description": "Custom Development", "price": 2000}],
            "customer_name": customer_name,
            "invoice_number": invoice_number,
        }
        return invoice
    # ... what if we add more types?

This approach has several major flaws:

  1. High Coupling: The client code is tightly coupled to the concrete implementation of every invoice type.
  2. Violation of DRY (Don’t Repeat Yourself): The header, footer, and other common attributes are repeated in every block.
  3. Maintenance Nightmare: If you need to change the default header, you have to update it in every single if block across the entire application.

The Solution: The Prototype Pattern

The Prototype pattern solves this by establishing a set of “template” objects. When you need a new object, you simply ask a template to clone itself, and then you modify the few unique details on the clone.

Here’s a visual breakdown of the concept:

graph TD
    subgraph Prototype Pattern
        A[Prototype Instance <br/> (e.g., Subscription Invoice Template)] -->|clones| B(New Object 1);
        A -->|clones| C(New Object 2);
        A -->|clones| D(New Object 3);
    end

    subgraph Customization
        B --> B1{Set customer='Client A', number=101};
        C --> C1{Set customer='Client B', number=102};
        D --> D1{Set customer='Client C', number=103};
    end

Step-by-Step Implementation in Python

Let’s refactor our invoicing system using the Prototype pattern. We’ll need a prototype “interface,” concrete classes, and a registry to manage our prototypes.

Step 1: Define the Project Structure

A clean structure helps separate concerns. Our prototype, registry, and client code will live in different files.

invoicing_system/
├── prototypes.py   # Contains the Prototype ABC and concrete Invoice classes
├── registry.py     # Manages our pre-configured invoice templates
└── main.py         # The client code that uses the pattern

Step 2: Create the Prototype “Interface”

In Python, we use Abstract Base Classes (ABC) to define interfaces. Our Prototype will have a single abstract method: clone().

File: prototypes.py

import copy
from abc import ABC, abstractmethod
from dataclasses import dataclass, field
from typing import List, Any

@dataclass
class InvoiceItem:
    """A single line item in an invoice."""
    description: str
    price: float

class Prototype(ABC):
    """The base Prototype class defines the cloning interface."""
    @abstractmethod
    def clone(self) -> Any:
        """Creates a copy of the object."""
        pass

@dataclass
class Invoice(Prototype):
    """The Concrete Prototype: An invoice that can be cloned."""
    customer_name: str
    invoice_number: int
    header: str
    footer: str
    tax_rate: float
    items: List[InvoiceItem] = field(default_factory=list)

    def clone(self) -> 'Invoice':
        """
        Creates a deep copy of the invoice.
        This is crucial for objects with nested mutable structures like lists.
        """
        return copy.deepcopy(self)

    def calculate_total(self) -> float:
        """Calculates the total cost of the invoice."""
        subtotal = sum(item.price for item in self.items)
        return subtotal * (1 + self.tax_rate)

[!TIP] Why copy.deepcopy? The Invoice class contains a list of InvoiceItem objects. A simple shallow copy (copy.copy()) would create a new Invoice but share the same list of items. Modifying the items in a cloned invoice would accidentally change the original template! copy.deepcopy recursively copies all objects, ensuring the clone is completely independent.

Step 3: Create a Prototype Registry

The registry is a central place to store and retrieve our pre-configured invoice templates. A simple dictionary is perfect for this.

File: registry.py

from prototypes import Invoice, InvoiceItem

class InvoiceRegistry:
    """A registry to store and manage invoice prototypes."""
    def __init__(self):
        self._prototypes = {}

    def register(self, name: str, prototype: Invoice):
        """Register a new prototype."""
        self._prototypes[name] = prototype

    def get(self, name: str) -> Invoice:
        """Get a prototype by name."""
        prototype = self._prototypes.get(name)
        if not prototype:
            raise ValueError(f"Prototype with name '{name}' not found.")
        return prototype.clone()

# --- Pre-configure and register our templates ---
DEFAULT_HEADER = "10xdev Blog - Official Invoice"
DEFAULT_FOOTER = "Thank you for your business!"

# Create the registry instance
invoice_registry = InvoiceRegistry()

# Subscription Template
subscription_prototype = Invoice(
    customer_name="",
    invoice_number=0,
    header=DEFAULT_HEADER,
    footer=DEFAULT_FOOTER,
    tax_rate=0.15,
    items=[InvoiceItem(description="Monthly Pro Subscription", price=100.0)]
)
invoice_registry.register("subscription", subscription_prototype)

# Hosting Template
hosting_prototype = Invoice(
    customer_name="",
    invoice_number=0,
    header=DEFAULT_HEADER,
    footer=DEFAULT_FOOTER,
    tax_rate=0.15,
    items=[InvoiceItem(description="Cloud Hosting Service (1 Year)", price=1500.0)]
)
invoice_registry.register("hosting", hosting_prototype)

# Custom Development Template
custom_dev_prototype = Invoice(
    customer_name="",
    invoice_number=0,
    header=DEFAULT_HEADER,
    footer=DEFAULT_FOOTER,
    tax_rate=0.18, # Note the different tax rate
    items=[InvoiceItem(description="Custom Feature Development", price=2000.0)]
)
invoice_registry.register("custom_development", custom_dev_prototype)

Step 4: The Refactored Client Code

Now, the client code becomes incredibly simple and clean. It just asks the registry for a template, clones it, and fills in the unique details.

File: main.py

from registry import invoice_registry
import datetime

def generate_customer_invoices(customers):
    """
    Generates invoices for a list of customers using the Prototype pattern.
    """
    generated_invoices = []
    for i, customer in enumerate(customers):
        # 1. Get the correct prototype from the registry and clone it
        new_invoice = invoice_registry.get(customer["type"])

        # 2. Customize the clone with dynamic data
        new_invoice.customer_name = customer["name"]
        new_invoice.invoice_number = 1000 + i
        # You could add more dynamic items if needed
        # new_invoice.items.append(InvoiceItem("Processing Fee", 5.0))

        generated_invoices.append(new_invoice)
        print(f"--- Generated Invoice #{new_invoice.invoice_number} ---")
        print(f"  Customer: {new_invoice.customer_name}")
        print(f"  Header: {new_invoice.header}")
        for item in new_invoice.items:
            print(f"  - {item.description}: ${item.price:.2f}")
        print(f"  Tax Rate: {new_invoice.tax_rate:.2%}")
        print(f"  Total: ${new_invoice.calculate_total():.2f}")
        print(f"  Footer: {new_invoice.footer}\n")

# --- Example Usage ---
customers_from_db = [
    {"name": "Client A", "type": "subscription"},
    {"name": "Client B", "type": "hosting"},
    {"name": "Client C", "type": "custom_development"},
]

generate_customer_invoices(customers_from_db)

Visualizing the Code Evolution

Using the Prototype pattern dramatically cleaned up our create_invoice logic. Here’s a “diff” view of the transformation:

- # The old, messy way
- def create_invoice(customer_type, customer_name, invoice_number):
-     if customer_type == "subscription":
-         invoice = {
-             "header": "Default Company Header",
-             "footer": "Default Company Footer",
-             "tax_rate": 0.15,
-             "items": [{"description": "Monthly Subscription", "price": 1000}],
-             "customer_name": customer_name,
-             "invoice_number": invoice_number,
-         }
-         return invoice
-     elif customer_type == "hosting":
-         # ... more repetitive code
-         pass

+ # The new, clean way with the Prototype pattern
+ def generate_customer_invoices(customers):
+     for i, customer in enumerate(customers):
+         # 1. Get a clone of the correct prototype
+         new_invoice = invoice_registry.get(customer["type"])
+
+         # 2. Customize the unique details
+         new_invoice.customer_name = customer["name"]
+         new_invoice.invoice_number = 1000 + i
+
+         # Ready to use!
+         print(f"Generated invoice for {new_invoice.customer_name}")

Class and Sequence Diagrams

To better understand the relationships and interactions, let’s look at the diagrams.

Class Diagram

This diagram shows how our classes are structured. Invoice implements the Prototype interface and the InvoiceRegistry holds instances of Invoice.

classDiagram
    class Prototype {
        <<ABC>>
        +clone()* Any
    }

    class Invoice {
        -customer_name: str
        -invoice_number: int
        -header: str
        -footer: str
        -tax_rate: float
        -items: List~InvoiceItem~
        +clone() Invoice
        +calculate_total() float
    }

    class InvoiceItem {
        -description: str
        -price: float
    }

    class InvoiceRegistry {
        -prototypes: dict
        +register(name, prototype)
        +get(name) Invoice
    }

    Prototype <|-- Invoice
    Invoice "1" *-- "many" InvoiceItem
    InvoiceRegistry ..> Invoice : uses

Sequence Diagram

This diagram illustrates the runtime interaction: the client asks the registry for an object, the registry clones its prototype, and returns the new copy.

sequenceDiagram
    participant Client as main.py
    participant Registry as InvoiceRegistry
    participant Prototype as subscription_prototype
    participant NewInvoice as new_invoice (Clone)

    Client->>Registry: get("subscription")
    activate Registry
    Registry->>Prototype: clone()
    activate Prototype
    Prototype-->>Registry: returns deep copy
    deactivate Prototype
    Registry-->>Client: returns new_invoice
    deactivate Registry

    Client->>NewInvoice: Set customer_name
    Client->>NewInvoice: Set invoice_number

When Should You Use the Prototype Pattern?

The Prototype pattern is most effective in the following scenarios:

  1. When object creation is expensive: If creating an object involves costly operations (e.g., database calls, network requests, heavy computation), you can create it once, cache it, and then clone it for subsequent uses.
  2. When your code shouldn’t depend on concrete classes: The client code can work with any object that conforms to the Prototype interface, without needing to know its specific class.
  3. When you have many object variations that only differ slightly: As seen in our invoice example, it’s much cleaner to have a few templates than to manage countless subclasses or complex constructors.

[!WARNING] Beware of Circular References copy.deepcopy is powerful, but it can get stuck in an infinite loop if objects reference each other (e.g., A.child = B and B.parent = A). While modern Python handles this, it’s a potential edge case to be aware of in complex object graphs.

Conclusion

The Prototype pattern offers a powerful and elegant solution to the problem of complex object creation. By shifting from a “creation from scratch” mindset to a “clone and customize” approach, you can significantly reduce code duplication, decouple your system, and make your application more flexible and maintainable. The next time you find yourself writing a long chain of if/elif/else to build objects, consider if a prototype could be your blueprint for cleaner code.


Join the 10xdev Community

Subscribe and get 8+ free PDFs that contain detailed roadmaps with recommended learning periods for each programming language or field, along with links to free resources such as books, YouTube tutorials, and courses with certificates.

Audio Interrupted

We lost the audio stream. Retry with shorter sentences?