Prototype Pattern — Deep Dive

How Python cloning works under the hood

When you call copy.copy(obj), Python follows this resolution order:

  1. Check for obj.__copy__() — if defined, call it
  2. For classes, create a new instance via cls.__new__(cls) and copy __dict__
  3. For built-in types, use type-specific fast paths

copy.deepcopy(obj) follows a similar chain but also tracks a memo dictionary to handle circular references and shared sub-objects.

import copy

class Config:
    def __init__(self, settings: dict, plugins: list[str]):
        self.settings = settings
        self.plugins = plugins

    def __copy__(self):
        # Shallow: share settings dict, copy plugins list
        new = self.__class__.__new__(self.__class__)
        new.settings = self.settings
        new.plugins = list(self.plugins)  # defensive copy of top-level list
        return new

    def __deepcopy__(self, memo):
        new = self.__class__.__new__(self.__class__)
        memo[id(self)] = new  # register before recursing to handle cycles
        new.settings = copy.deepcopy(self.settings, memo)
        new.plugins = copy.deepcopy(self.plugins, memo)
        return new

Implementing __deepcopy__ with the memo parameter is critical when objects have circular references. Without it, deepcopy would recurse infinitely.

Prototype registry implementation

A production-grade registry stores named prototypes and clones on demand:

import copy
from dataclasses import dataclass, field


@dataclass
class ServerConfig:
    hostname: str
    port: int
    tls: bool
    headers: dict[str, str] = field(default_factory=dict)
    middlewares: list[str] = field(default_factory=list)


class ConfigRegistry:
    def __init__(self):
        self._prototypes: dict[str, ServerConfig] = {}

    def register(self, name: str, prototype: ServerConfig) -> None:
        self._prototypes[name] = prototype

    def create(self, name: str, **overrides) -> ServerConfig:
        if name not in self._prototypes:
            raise KeyError(f"No prototype registered as '{name}'")
        clone = copy.deepcopy(self._prototypes[name])
        for attr, value in overrides.items():
            if not hasattr(clone, attr):
                raise AttributeError(f"ServerConfig has no attribute '{attr}'")
            setattr(clone, attr, value)
        return clone


# Setup
registry = ConfigRegistry()
registry.register("production", ServerConfig(
    hostname="api.example.com",
    port=443,
    tls=True,
    headers={"X-Request-ID": "auto"},
    middlewares=["auth", "rate-limit", "logging"],
))
registry.register("development", ServerConfig(
    hostname="localhost",
    port=8000,
    tls=False,
    headers={},
    middlewares=["logging"],
))

# Usage — clone and customize
staging = registry.create("production", hostname="staging.example.com", port=8443)

The registry centralizes configuration. Teams don’t need to know every field — they pick a base and override what’s different.

Performance: when cloning outperforms construction

Benchmarking matters here. Deepcopy has overhead — it walks the entire object graph. For simple flat objects, construction via __init__ is often faster. The Prototype Pattern pays off when:

  • Initialization has side effects (database calls, file reads, network requests)
  • The object graph is deep but mostly shared — deepcopy with memo avoids re-copying shared sub-objects
  • You need many variants quickly — game engines creating hundreds of enemy instances per frame
import copy
import time

class ExpensiveResource:
    def __init__(self):
        time.sleep(0.01)  # simulate costly initialization
        self.data = list(range(1000))

# Construction: ~10ms each
resources_constructed = [ExpensiveResource() for _ in range(100)]

# Prototype: ~10ms once, then microseconds per clone
prototype = ExpensiveResource()
resources_cloned = [copy.deepcopy(prototype) for _ in range(100)]

In real-world benchmarks with database-backed initialization, cloning can be 10-100x faster than reconstruction.

Prototype with __init_subclass__ auto-registration

Combine with Python’s class hooks for automatic prototype registration:

import copy

class AutoPrototype:
    _registry: dict[str, "AutoPrototype"] = {}

    def __init_subclass__(cls, proto_name: str = "", **kwargs):
        super().__init_subclass__(**kwargs)
        if proto_name:
            AutoPrototype._registry[proto_name] = cls()

    @classmethod
    def clone(cls, name: str) -> "AutoPrototype":
        return copy.deepcopy(cls._registry[name])


class DefaultEnemy(AutoPrototype, proto_name="enemy"):
    def __init__(self):
        self.health = 100
        self.speed = 5
        self.inventory = ["sword"]


class DefaultNPC(AutoPrototype, proto_name="npc"):
    def __init__(self):
        self.health = 50
        self.speed = 3
        self.inventory = ["potion"]


enemy = AutoPrototype.clone("enemy")
enemy.health = 80  # customize without affecting prototype

Prototype vs Factory vs Builder

AspectPrototypeFactoryBuilder
Creates viaCloning existing instanceCalling constructor of chosen classStep-by-step configuration
Best whenSetup is expensiveMultiple subclasses to choose fromMany optional parameters
State sourceCopied from prototypeFresh from constructorAccumulated by builder
CustomizationPost-clone modificationConstructor argumentsMethod chain

These patterns compose well. A factory can return cloned prototypes. A builder can use a prototype as a starting point.

Pitfalls in production

Shallow copy sharing mutable state

The most common bug. If your prototype has a list and you shallow-copy it, both objects share that list. Always audit which fields are mutable containers.

Deepcopy of unpicklable objects

deepcopy uses a protocol similar to pickle. Objects holding file handles, database connections, or locks will fail. Implement __deepcopy__ to handle these — typically by creating a fresh resource or setting it to None in the clone.

def __deepcopy__(self, memo):
    new = self.__class__.__new__(self.__class__)
    memo[id(self)] = new
    new.config = copy.deepcopy(self.config, memo)
    new._connection = None  # don't clone the connection
    return new

Thread safety

If multiple threads clone from the same prototype simultaneously, deepcopy is safe (it doesn’t modify the original). But if threads modify the prototype while others are cloning, you need synchronization.

The one thing to remember: Python’s copy module gives you Prototype Pattern for free — use __copy__ and __deepcopy__ to control exactly what gets cloned and what gets recreated, matching your performance and safety needs.

pythondesign-patternsoop

See Also

  • Python Adapter Pattern How Python's Adapter Pattern works like a travel power plug — making incompatible things work together.
  • Python Bridge Pattern Why separating what something does from how it does it keeps your Python code from becoming a tangled mess.
  • Python Builder Pattern Why building complex Python objects step by step beats cramming everything into one giant constructor.
  • Python Composite Pattern How the Composite Pattern lets you treat a group of things the same way you'd treat a single thing in Python.
  • Python Facade Pattern How the Facade Pattern gives you one simple button instead of a confusing control panel in Python.