Prototype Pattern — Deep Dive
How Python cloning works under the hood
When you call copy.copy(obj), Python follows this resolution order:
- Check for
obj.__copy__()— if defined, call it - For classes, create a new instance via
cls.__new__(cls)and copy__dict__ - 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
| Aspect | Prototype | Factory | Builder |
|---|---|---|---|
| Creates via | Cloning existing instance | Calling constructor of chosen class | Step-by-step configuration |
| Best when | Setup is expensive | Multiple subclasses to choose from | Many optional parameters |
| State source | Copied from prototype | Fresh from constructor | Accumulated by builder |
| Customization | Post-clone modification | Constructor arguments | Method 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.
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.