Some objects are expensive to set up: they load textures, read config, or run heavy initialization. Prototype lets you configure one object once and then create new ones by copying it, tweaking the copy if needed.
Intent#
Specify the kinds of objects to create using a prototypical instance, and create new objects by copying this prototype. (GoF)
Analogy: a photocopier. Each copy is a new document with the same content, and it's much faster than retyping the original.
The problem: repeating expensive setup#
Without a prototype, every new monster repeats the whole setup:
monster1 = GameCharacter()
monster1.health = 100
monster1.armor = 50
monster1.weapon = "Sword"
monster1.load_textures() # expensive
monster1.load_animations() # expensive
monster1.setup_ai() # expensive
monster2 = GameCharacter()
monster2.health = 100 # repeat everything...
monster2.load_textures() # ...including the expensive parts
Prototype also helps when you only learn at runtime which kinds of objects you need, or when there are too many configurations to justify a subclass for each.
How it works#
The recipe has three steps:
- Create and fully configure a prototype instance.
- Clone it to make new instances.
- Modify the clone where it should differ.
| Participant | Role |
|---|---|
| Prototype | Declares clone() |
| Concrete prototype | Implements clone(), copying its own state |
| Client | Asks a prototype to clone itself instead of calling the constructor |
A prototype registry (a map from name to prototype) is a common addition: the client asks for "orc" and gets a fresh clone of the stored orc.
Shallow vs deep copy#
This is the part that bites people.
- Shallow copy: copies the object's attributes, but attributes that refer to other objects still point to the same nested objects.
- Deep copy: also creates new copies of the nested objects.
copy.copy() is shallow.
import copy
class Address:
def __init__(self, city: str):
self.city = city
class Person:
def __init__(self, name: str, address: Address):
self.name = name
self.address = address
def clone(self) -> "Person":
return copy.copy(self) # SHALLOW
person1 = Person("John", Address("New York"))
person2 = person1.clone()
person2.address.city = "Boston"
print(person1.address.city) # Boston: the original changed!
After a shallow copy both objects point at the same Address; after a deep copy each has its own.
To deep copy, use copy.deepcopy(), which recursively copies nested objects:
class DeepPerson(Person):
def clone(self) -> "DeepPerson":
return copy.deepcopy(self) # nested Address is copied too
person1 = DeepPerson("John", Address("New York"))
person2 = person1.clone()
person2.address.city = "Boston"
print(person1.address.city) # New York: the original is untouched
Strings are immutable, so sharing name is safe. Only mutable nested objects need deep copying. To control what gets copied, define __copy__ or __deepcopy__ on the class.
Example: spawning enemies from a registry#
Python's copy module gives you both kinds of copy: copy.copy() (shallow) and copy.deepcopy() (deep).
import copy
class Enemy:
def __init__(self, kind: str, health: int, abilities: list[str]):
self.kind = kind
self.health = health
self.abilities = abilities
self._load_assets() # pretend this is slow
def _load_assets(self) -> None:
print(f"Loading textures for {self.kind}...")
def clone(self) -> "Enemy":
# deepcopy so each clone gets its own abilities list,
# and __init__ (with the slow asset load) is NOT re-run
return copy.deepcopy(self)
class EnemyRegistry:
def __init__(self) -> None:
self._prototypes: dict[str, Enemy] = {}
def register(self, name: str, prototype: Enemy) -> None:
self._prototypes[name] = prototype
def spawn(self, name: str) -> Enemy:
return self._prototypes[name].clone()
registry = EnemyRegistry()
registry.register("orc", Enemy("orc", 100, ["club"])) # assets loaded once
orc1 = registry.spawn("orc")
orc2 = registry.spawn("orc")
orc2.abilities.append("rage")
print(orc1.abilities) # ['club']: the deep copy kept them independent
Two ways to implement it#
Python gives you two common options.
- The
copymodule:copy.copy()orcopy.deepcopy(), optionally customized with__copy__/__deepcopy__. Note that neither calls__init__, so slow setup isn't repeated. - An explicit copy method: a
copy()method or afrom_other(cls, other)class method that copies each attribute by hand. It gives more control, and for dataclassesdataclasses.replace(obj, **changes)does this for you.
Real-world examples#
Cloning shows up in languages and applications alike:
copy.copy/copy.deepcopy, anddict.copy()/list.copy()(both shallow).- JavaScript is a prototype-based language: objects inherit directly from other objects.
- Game enemy spawning, document templates, cloning a base configuration, duplicating a styled UI component.
When to use it#
Prototype pays off when:
- Creating an object from scratch is expensive (loading resources, heavy computation).
- You need many similar objects with small variations.
- The types to create are decided at runtime (for example, loaded from a registry).
- Subclassing every variation would cause a class explosion.
When not to use it#
Skip it when:
- Construction is cheap. Just call the constructor.
- Every object is quite different from the others.
- The object is immutable: you can share the same instance instead of copying it.
- The object graph is deep and full of cycles, which makes correct cloning hard.
Pitfalls#
Most Prototype bugs are copying bugs.
- Confusing shallow and deep copy, then mutating shared state.
- Forgetting to deep-copy a newly added mutable field.
- Circular references causing infinite recursion in a hand-written deep copy (
copy.deepcopyhandles cycles with a memo; hand-rolled code often doesn't). - Every class in the hierarchy needs a correct
clone(), and subclasses can easily break it.
Prototype vs other creational patterns#
Each creational pattern answers "where does the object come from?" differently.
| Pattern | How the object comes to be |
|---|---|
| Factory Method | Created from scratch by a constructor call |
| Builder | Assembled step by step |
| Prototype | Copied from an existing instance in one go |
| Singleton | Exactly one instance; the opposite of copying |
Key takeaways#
- Prototype creates objects by cloning a configured instance instead of building from scratch.
copy.copy()is shallow: nested mutable objects are shared.- Deep-copy every mutable nested object, with
copy.deepcopyor an explicit copy method. - A registry of prototypes lets you pick what to create at runtime.
- Skip it for cheap or immutable objects.