bigonotes
CS ConceptsDesign Patterns

Prototype Pattern

Create new objects by cloning a configured prototype, and understand why shallow copies are dangerous.

5 min readLast updated Design Patterns

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:

  1. Create and fully configure a prototype instance.
  2. Clone it to make new instances.
  3. 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.

Shallow copy: person1 and person2 both point at one shared Address. Deep copy: each Person points at its own Address

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.

  1. The copy module: copy.copy() or copy.deepcopy(), optionally customized with __copy__ / __deepcopy__. Note that neither calls __init__, so slow setup isn't repeated.
  2. An explicit copy method: a copy() method or a from_other(cls, other) class method that copies each attribute by hand. It gives more control, and for dataclasses dataclasses.replace(obj, **changes) does this for you.

Real-world examples#

Cloning shows up in languages and applications alike:

  • copy.copy / copy.deepcopy, and dict.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.deepcopy handles 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.deepcopy or an explicit copy method.
  • A registry of prototypes lets you pick what to create at runtime.
  • Skip it for cheap or immutable objects.

Found a mistake? Tell me on LinkedIn ↗

Further reading#