Course contents

Prototype — clone the template, then tweak

Sign in to run this lab and save your progress

Learn

Before you start

Prototype is the least flashy creational pattern, but it has the clearest use case: slow to build + a small set of templates + independent copies.

If any of those three is missing, don't use Prototype.

The signal to use it

Look at the code that builds a new object. Ask:

  1. Does this take real time / memory / disk I/O?
  2. Am I building nearly the same thing over and over, changing only 10-20% of the fields?
  3. Do the resulting objects need to be independent (changing one shouldn't touch another)?

Three yeses → Prototype. Otherwise → just build fresh.

Where Prototype shows up in design problems

Real examples that come up:

  • Game character templates. (This lesson.)
  • Config templates. A base "dev" config that gets copied and tweaked per environment.
  • HTTP request templates. A pre-configured client with auth headers + timeout policy; each real request copies it and adds path + body.
  • Report templates. Pre-loaded chart definitions copied and filled with per-report data.
  • UI dashboard widgets. Pre-built widgets copied and dropped into a user's dashboard.

Prototype often comes with a Registry (like our Design D). The registry becomes the "list of templates" the app knows about.

The deepcopy subtlety

copy.deepcopy is Python's built-in for making an independent copy of an object. It handles most cases, but has trouble with:

  • File handles / sockets / DB connections — copying them doesn't copy the underlying resource. Usually a bug. Fix: write a __deepcopy__ method that skips or re-opens them.
  • Objects with cycles — deepcopy handles cycles correctly (it tracks what it's seen), so mostly fine. But performance can suffer on huge graphs.
  • __slots__ classes — deepcopy usually works, but edge cases fail quietly.
  • Objects with __reduce__ or unusual pickle behavior — surprises can happen.

For "plain data" objects (dicts, lists, primitives, nested objects of the same kind), deepcopy Just Works. That's why Prototype is most common in games and configuration systems — that's where the objects tend to be plain data.

The escape hatch: __deepcopy__

For objects with expensive-to-copy or uncopyable parts, write a __deepcopy__(self, memo) method that controls the copy:

class Character:
    def __init__(self, name, ...):
        self.name = name
        # a big cached lookup we don't want to duplicate
        self._skill_tree_cache = _load_from_disk(name)

    def __deepcopy__(self, memo):
        new = Character(self.name, ...)
        # share the (never-changed) cache instead of copying it
        new._skill_tree_cache = self._skill_tree_cache
        return new

This is the way out when "expensive to build AND some parts shouldn't be copied". Most design problems don't need it, but knowing the escape hatch exists gets you out of jams.

Prototype vs Factory

Both build objects. Difference:

  • Factory — builds from CLASSES. VehicleFactory.create("CAR") runs Car.__init__.
  • Prototype — copies from EXISTING OBJECTS. PrototypeRegistry.spawn("warrior") copies an existing Warrior.

Use Prototype when the interesting state is on the INSTANCE (specific stat values, specific inventory items), not just the class. If all that varies is the class name, Factory is simpler.

The interview trap

Interviewers sometimes describe a problem in a way that sounds Prototype-shaped but isn't. Watch for:

  • "We need many similar objects" — is building actually slow, or are they cheap and just similar?
  • "We have templates" — are they CLASS templates (Factory) or INSTANCE templates (Prototype)?
  • "We want to customize each one" — how much? Small tweaks → Prototype. Big restructuring → Factory or Builder.

Ask the requirement questions from the objective before picking. That's the interview skill.

The decision cheat-sheet

Situation Pick
Cheap to build, no templates No pattern needed
Cheap to build, templates Factory + kwargs
Slow to build, only one instance Cache + reuse (not Prototype)
Slow to build, few templates, big customization Builder
Slow to build, few templates, small tweaks Prototype
Slow to build, growing template set, dynamic Prototype + Registry

Your task

Prototype answers one question: when building a new object from scratch is slow or complex, how do we make new ones cheaply?

Answer: keep a ready-made object as a template, and COPY it every time you need a new one.

The problem

A multiplayer game has a dozen character types — Warrior, Mage, Ranger, Healer, etc. Each has:

  • Base stats (strength, agility, intellect, wisdom — 20+ numbers)
  • A starting inventory (sword, shield, spellbook...)
  • A default skill tree (30 nodes)
  • Class-specific things (a Warrior's rage bar; a Mage's mana pool)

Building a Warrior from scratch means running Warrior.__init__ which computes stats from tables, loads the inventory from a JSON file, and builds the skill tree node by node. It takes about 50 ms. Doing that 100 times per matchmaking batch is 5 seconds — slow enough for players to notice.

The kicker: 90% of players use the DEFAULT stats and inventory. A few change 1-2 things (a Warrior who puts 3 points into agility). Almost nobody rebuilds from scratch — they just tweak the template.

Questions to ask about the requirements

  1. Is building slow? Slow enough to notice? If not, just build fresh — you don't need Prototype.
  2. Is there a small set of "templates" everyone starts from? If every object is unique, copying doesn't help.
  3. Are the copies INDEPENDENT of each other? If changing one copy would corrupt another, we need DEEP copies.
  4. Are the templates known at code time, or loaded from config while the app runs?
  5. How much of the object changes after copying? Small tweaks → Prototype wins. Big changes → maybe just rebuild.

Answers for our game: yes slow, yes small template set (12 types), yes independent (each player's Warrior is their own), loaded from JSON, tweaks are small (change 1-2 stats or swap one item). Prototype fits.

Different designs, compared

Design A — build fresh every time (no prototype)

class Warrior:
    def __init__(self):
        self.stats = _compute_warrior_stats()     # slow
        self.inventory = _load_warrior_inventory()  # slow
        self.skill_tree = _build_warrior_skill_tree()  # slow

w = Warrior()   # 50ms
  • Good: Simplest. No new abstractions.
  • Bad: Slow at scale. Nothing gets reused.
  • When it wins: Building is cheap. Small number of objects. Every object is genuinely unique.

Design B — factory that caches on the first call

class Warrior:
    _template = None
    @classmethod
    def create(cls):
        if cls._template is None:
            cls._template = _load_from_disk()
        # ...somehow copy _template and return?
  • Good: Caches the slow load.
  • Bad: How do we return a NEW object from _template? If we return _template itself, changes leak. If we deepcopy _template inside create(), we've re-invented Prototype — just uglier. If we serialize and reload, we're back to slow.
  • When it wins: Never, cleanly. Turns into Prototype the moment you take it seriously.

Design C — Prototype with a clone() method

class Warrior:
    def __init__(self):
        # slow setup ONCE
        self.stats = _compute_warrior_stats()
        self.inventory = _load_warrior_inventory()

    def clone(self):
        # deep-copy so changes to the copy don't touch me
        return copy.deepcopy(self)


# At app startup:
ARCHETYPES = {
    "warrior": Warrior(),
    "mage": Mage(),
    # ... 10 more
}

# Later, when a new player joins:
player_warrior = ARCHETYPES["warrior"].clone()
player_warrior.stats.agility += 3
  • Good: Each template is built ONCE. clone() is fast (no disk I/O). Tweak-then-play flow is natural.
  • Bad: deepcopy isn't magic — objects with cycles, external handles (files, sockets), or unpicklable state need custom __deepcopy__ methods.
  • When it wins: All the requirements we gathered.

Design D — Prototype registry with register() + spawn()

class PrototypeRegistry:
    _prototypes = {}

    @classmethod
    def register(cls, name, prototype):
        cls._prototypes[name] = prototype

    @classmethod
    def spawn(cls, name):
        proto = cls._prototypes.get(name)
        if proto is None:
            raise ValueError(f"no prototype {name!r}")
        return proto.clone()

Callers use PrototypeRegistry.spawn("warrior").

  • Good: All of Design C's benefits, plus one clean "look-up-and-copy" API. Adding a new type (loaded from a mod / DLC / config file) is one register() call.
  • Bad: More setup work. If you only have 2-3 fixed types and no dynamic loading, Design C is lighter.
  • When it wins: Growing set of types, some loaded while the app runs, want one "spawn a X" API.

The locked-in choice: Design D

Reviewing requirements:

  • The cost is real (50ms × 100 = 5 seconds). → Rules out A.
  • Design B falls apart into Design C anyway. C is the honest version.
  • 12 types, growing (DLC will add more, config file for community mods). The registry helps. → Picks D over C.
  • Independent copies needed. Both C and D use deepcopy.

We build Design D. If we only had 2 types, we'd pick Design C.

Mechanics for Design D

import copy

class Character:
    def __init__(self, name, stats, inventory):
        self.name = name
        self.stats = dict(stats)         # {"str": 10, ...}
        self.inventory = list(inventory) # ["sword", "shield"]

    def clone(self):
        return copy.deepcopy(self)


class PrototypeRegistry:
    _prototypes = {}

    @classmethod
    def register(cls, name, prototype):
        cls._prototypes[name] = prototype

    @classmethod
    def spawn(cls, name):
        proto = cls._prototypes.get(name)
        if proto is None:
            raise ValueError(f"no prototype {name!r}")
        return proto.clone()

    @classmethod
    def _reset(cls):
        cls._prototypes = {}


# At startup:
PrototypeRegistry.register(
    "warrior",
    Character(
        name="Warrior",
        stats={"str": 15, "agi": 8, "int": 5, "wis": 5},
        inventory=["longsword", "shield", "leather-armor"],
    ),
)

# At play:
p1 = PrototypeRegistry.spawn("warrior")
p1.stats["agi"] += 3        # customize
p2 = PrototypeRegistry.spawn("warrior")
# p2 is unaffected by p1's mutation — deepcopy did its job

Your task

Build in main.py:

  1. Character class with:
    • __init__(self, name, stats, inventory) — store name, and take internal SNAPSHOTS: self.stats = dict(stats), self.inventory = list(inventory). (Snapshots avoid shared references with the caller's originals.)
    • clone(self) — returns a copy.deepcopy(self)
  2. PrototypeRegistry class with:
    • _prototypes class attribute (starts empty)
    • register(name, prototype) classmethod
    • spawn(name) classmethod — clones the registered prototype, raises ValueError for unknown names
    • _reset() classmethod — clears the registry (tests)

Structural tests enforce:

  • spawn returns a genuinely new object (clone is not proto).
  • Mutating a spawn's stats/inventory does NOT affect the registered prototype (deep copy verified).
  • Two spawns of the same archetype are independent (mutating one doesn't affect the other).
  • Unknown archetype names raise ValueError.

Click Submit to run the tests.

Starter files

Preview only — open the lab to edit and run.

files_dir
05-prototype-starter