Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Design Patterns (GoF and modern)  Module 2 – Creational patterns: Factory Method to Singleton

Prototype: create objects by copying

Prototype makes new objects by copying configured ones. A prototype registry, deep and shallow copies, and what each of six languages copies or shares.

  • Intermediate
  • 30 minutes
  • Examples run with Python 3.14.8, Pyodide 314.0.7, Node.js 24.21.0, Java 25, Go 1.27.1, Clang 21.0.0 and Rust 1.99.0
  • By MySmartCoPilot
Free: 30 minutes

What you will learn

  • Implement Prototype with explicit, tested shallow and deep copies
  • Predict what each language's copy facility copies and what it shares
  • Use a prototype registry to create preconfigured objects
  • Avoid the pitfalls of Java's Cloneable and C++ object slicing

Before you start

What you’ll learn · Before you start

What you will learn

  • Implement Prototype with explicit, tested shallow and deep copies
  • Predict what each language's copy facility copies and what it shares
  • Use a prototype registry to create preconfigured objects
  • Avoid the pitfalls of Java's Cloneable and C++ object slicing

Before you start

On this page

Prototype is a creational design pattern that makes new objects by copying an existing, fully configured one, the prototype, instead of building each from scratch. The client asks the prototype for a copy, so it needs neither the concrete class nor the steps that configured it. That pays off when setting an object up is expensive or takes many settings, or when the class to create is only known while the program runs, such as the template a user picked. Every Prototype comes down to one question: what does a copy share with its original? Get that wrong and changing a copy quietly changes the prototype, and with it every object copied from it later.

This lesson builds a registry of document prototypes in Python, reproduces the shallow-copy bug, and shows what the copy facilities of JavaScript, Java, Go, C++ and Rust copy and what they share, each with a run you can read. It ends with the cases where copying is the wrong tool.

The problem: the same set-up, again and again

An invoicing program has three kinds of document, an invoice, a receipt and a quotation, and each comes with a title, payment terms, a house style and an empty list of lines. Building a fresh one means repeating every setting, or a constructor with all of them, or a subclass per kind. And the kinds are not fixed: a shop owner can design a “Proforma invoice” in the app itself, at run time, which no class in the code knows about.

Copying solves both. Configure each kind once, keep the configured object, and make every new document as a copy of it. A user-designed kind is just one more configured object. The cost moves to the copy, which must give each new document its own changeable parts while sharing what may be shared.

Shallow and deep copies

A shallow copy shares the prototype’s list of lines, a deep copy has its own list, and all three share one immutable Style.shallow copycopy.copyprototypedeep copyclone()lines(one shared list)lines(its own list)Style(immutable, shared)

What a shallow copy and a deep copy share with the prototype

Text description of the diagram

The diagram shows three objects at the top and what each one refers to below.

  • The prototype, the invoice template, and its shallow copy (made with copy.copy) both point at the same lines list. A line added through either one appears in both.
  • The deep copy (made with clone) points at a lines list of its own, so changes to its lines never reach the prototype.
  • All three point at the same Style object. Style is immutable, so sharing it is safe and saves copying.

The Python documentation draws the line clearly. A shallow copy is a new object that holds references to the same inner objects as the original; a deep copy is a new object that holds copies of them, made recursively. The difference matters only for compound objects, those that contain lists, dictionaries or other objects:

  • A shallow copy of a document shares its list of lines, so a line added through the copy shows up in the prototype.
  • A deep copy has its own list, so the prototype is safe.
  • Neither needs to copy an immutable part. A Style that nothing can change may be shared by every copy, which saves memory and keeps a style change in one place. Copying “everything” is as much a mistake as copying too little.

Prototype in Python: a registry of templates

A prototype registry, a shallow copy and deep copies Python · templates/templates.py
"""Prototype: new documents are copies of configured prototypes kept in a registry, never built
from scratch."""

import copy
from dataclasses import dataclass


@dataclass(frozen=True)
class Style:
    """Immutable, so every copy may share one Style object."""

    font: str
    accent: str

    def __deepcopy__(self, memo):
        return self  # nothing in it can change, so a deep copy keeps this very object


class Document:
    def __init__(self, kind, style, fields, lines=()):
        self.kind = kind
        self.style = style
        self.fields = dict(fields)
        self.lines = list(lines)
        self.related = []  # other documents, which may point back at this one

    def clone(self, **changes):
        """A deep copy of this prototype with some fields changed."""
        doc = copy.deepcopy(self)
        doc.fields.update(changes)
        return doc

    def __repr__(self):
        n = len(self.lines)
        number, buyer = self.fields.get("number", "-"), self.fields.get("buyer", "-")
        return f"{self.kind} {number} for {buyer}, {n} line{'' if n == 1 else 's'}"


class Registry:
    """Named prototypes; create() hands out copies."""

    def __init__(self):
        self._prototypes = {}

    def add(self, name, prototype):
        self._prototypes[name] = prototype

    def create(self, name, **changes):
        return self._prototypes[name].clone(**changes)


house = Style(font="Inter", accent="teal")
registry = Registry()
registry.add("invoice", Document("invoice", house, {"title": "Tax invoice", "terms": "15 days"}))
registry.add("receipt", Document("receipt", house, {"title": "Receipt", "terms": "Paid in full"}))
registry.add("quotation", Document("quotation", house, {"title": "Quotation", "terms": "30 days"}))

# 1. The registry: each document starts as a copy of a configured prototype.
invoice = registry.create("invoice", number="INV-1042", buyer="Asha Traders")
invoice.lines.append(("kettle", 2, 129900))
quote = registry.create("quotation", number="QUO-88", buyer="Ravi Stores")
print(invoice, "|", quote)
print("prototypes untouched:", registry.create("invoice"))

# 2. A shallow copy shares the lists inside: the prototype grows with its copy.
proto = Document("invoice", house, {"title": "Tax invoice"})
shallow = copy.copy(proto)
shallow.lines.append(("mug", 4, 24950))
print(f"copy.copy: the prototype now has {len(proto.lines)} line, added through the copy")

# 3. A deep copy owns its lists, and shares the immutable Style on purpose.
proto = Document("invoice", house, {"title": "Tax invoice"})
deep = proto.clone()
deep.lines.append(("mug", 4, 24950))
shared = deep.style is proto.style
print(f"deepcopy: the prototype keeps {len(proto.lines)} lines; style shared: {shared}")

# 4. Cycles: the memo dictionary maps each original to its copy, so references back are copied
#    once.
paid = Document("receipt", house, {"number": "RCT-7"})
billed = Document("invoice", house, {"number": "INV-1043"})
paid.related.append(billed)
billed.related.append(paid)
twin = copy.deepcopy(billed)
print(f"cycle: the copy's receipt points back to the copy: {twin.related[0].related[0] is twin}")

Output

invoice INV-1042 for Asha Traders, 1 line | quotation QUO-88 for Ravi Stores, 0 lines
prototypes untouched: invoice - for -, 0 lines
copy.copy: the prototype now has 1 line, added through the copy
deepcopy: the prototype keeps 0 lines; style shared: True
cycle: the copy's receipt points back to the copy: True

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 templates.py

Line by line:

  1. Registry.create() hands out deep copies of configured prototypes, with some fields changed: an invoice and a quotation that each start from their kind’s settings. A line added to the invoice stays in the invoice, and the prototype in the registry still has none.
  2. copy.copy() makes a shallow copy, and appending to its lines changed the prototype: the bug this pattern is known for.
  3. clone() uses copy.deepcopy(), so the copy owns its lists. Style defines __deepcopy__() to return itself, which is how a class tells deepcopy that it may be shared: the copy’s style is the prototype’s style.
  4. The receipt and the invoice refer to each other. deepcopy() keeps a memo dictionary of the objects it has already copied in this call, so when the copy of the receipt points back at the invoice, it points at the copy of the invoice, and the cycle does not recurse forever.

Python also offers copy.replace() (since Python 3.13) for copying a dataclass or named tuple with some fields changed; it is the right tool for immutable values, while the documents here are changeable objects.

What each language copies, and what it shares

Every language has a built-in way to copy, and each one shares something. The programs below make the same experiment: copy a template, change the copy, and look at the prototype.

The copy facilities of Java, TypeScript, Go, C++ and Rust

Java · compiled/Templates.java

// Prototype in Java 25: what Object.clone() shares, and a copy constructor that decides field
// by field.
// Run: java Templates.java
import java.util.ArrayList;
import java.util.List;

public class Templates {
    record Style(String font, String accent) {} // immutable, so copies may share it

    // 1. Cloneable: Object.clone() fills the new object's fields "as if by assignment", so the
    //    list is shared.
    static class CloneableTemplate implements Cloneable {
        final Style style;
        // final: clone() cannot even replace it with a copy
        final List<String> lines = new ArrayList<>();

        CloneableTemplate(Style style) {
            this.style = style;
        }

        @Override
        public CloneableTemplate clone() {
            try {
                return (CloneableTemplate) super.clone();
            } catch (CloneNotSupportedException e) {
                throw new AssertionError(e);
            }
        }
    }

    // 2. A copy constructor: copy what can change, share what cannot.
    static final class Template {
        final Style style;
        final List<String> lines;

        Template(Style style) {
            this.style = style;
            this.lines = new ArrayList<>();
        }

        Template(Template prototype) {
            this.style = prototype.style;
            this.lines = new ArrayList<>(prototype.lines);
        }
    }

    public static void main(String[] args) {
        Style house = new Style("Inter", "teal");

        CloneableTemplate first = new CloneableTemplate(house);
        CloneableTemplate firstCopy = first.clone();
        firstCopy.lines.add("mug");
        boolean sameList = firstCopy.lines == first.lines;
        int firstLines = first.lines.size();
        System.out.println("Object.clone():   the prototype now has " + firstLines + " line;"
                + " same list: " + sameList);

        Template second = new Template(house);
        Template secondCopy = new Template(second);
        secondCopy.lines.add("mug");
        boolean shared = secondCopy.style == second.style;
        int secondLines = second.lines.size();
        System.out.println("copy constructor: the prototype keeps " + secondLines + " lines;"
                + " style shared: " + shared);
    }
}

TypeScript · compiled/templates.ts

// Prototype in TypeScript: structuredClone copies data but not the class; a spread copy is
// shallow; an explicit clone() method keeps the class and copies what can change.
// Run: node templates.ts
interface Style {
  readonly font: string;
  readonly accent: string;
}

class Template {
  readonly style: Style;
  lines: string[];

  constructor(style: Style, lines: string[] = []) {
    this.style = style;
    this.lines = lines;
  }

  count(): number {
    return this.lines.length;
  }

  clone(): Template {
    // a new list; the immutable style is shared
    return new Template(this.style, [...this.lines]);
  }
}

const house: Style = Object.freeze({ font: "Inter", accent: "teal" });

const a = new Template(house, ["kettle"]);
const structured = structuredClone(a);
const isTemplate = structured instanceof Template;
const kept = `instanceof Template: ${isTemplate}; count(): ${typeof structured.count}`;
console.log(`structuredClone: lines ${JSON.stringify(structured.lines)}; ${kept}`);

const b = new Template(house, ["kettle"]);
const spread = { ...b };
spread.lines.push("mug");
const spreadShares = spread.style === b.style;
const spreadLines = b.count();
console.log(
  `spread copy:     the prototype now has ${spreadLines} lines; style shared: ${spreadShares}`,
);

const c = new Template(house, ["kettle"]);
const cloned = c.clone();
cloned.lines.push("mug");
const cloneShares = cloned.style === c.style;
const facts = `instanceof Template: ${cloned instanceof Template}; style shared: ${cloneShares}`;
console.log(`clone():         the prototype keeps ${c.count()} line; ${facts}`);

Go · compiled/templates.go

// Prototype in Go: struct assignment copies a map or slice header, not its contents; maps.Clone
// and slices.Clone copy one level; an explicit Clone method decides what to copy.
// Run: go run templates.go
package main

import (
	"fmt"
	"maps"
	"slices"
)

type Style struct{ Font, Accent string }

type Template struct {
	Style  *Style // shared on purpose: nothing changes it
	Fields map[string]string
	Lines  []string
}

func (t *Template) Clone() *Template {
	return &Template{Style: t.Style, Fields: maps.Clone(t.Fields), Lines: slices.Clone(t.Lines)}
}

func prototype(style *Style) *Template {
	return &Template{
		Style:  style,
		Fields: map[string]string{"title": "Tax invoice"},
		Lines:  []string{"kettle"},
	}
}

func main() {
	house := &Style{"Inter", "teal"}

	p := prototype(house)
	shallow := *p // copies the struct: the map and the slice's backing array are shared
	shallow.Fields["title"] = "Receipt"
	shallow.Lines[0] = "mug"
	fmt.Printf("struct assignment: the prototype's title is now %q and its first line %q\n",
		p.Fields["title"], p.Lines[0])

	q := prototype(house)
	deep := q.Clone()
	deep.Fields["title"] = "Receipt"
	deep.Lines = append(deep.Lines, "mug")
	fmt.Printf("Clone():           the prototype keeps %q and %d line; style shared: %v\n",
		q.Fields["title"], len(q.Lines), deep.Style == q.Style)

	nested := map[string][]string{"lines": {"kettle"}}
	copied := maps.Clone(nested)
	copied["lines"][0] = "mug"
	fmt.Printf("maps.Clone:        a slice inside the cloned map is still shared: %q\n",
		nested["lines"][0])
}

C++ · compiled/templates.cpp

// Prototype in C++23: copying a derived object through a base reference slices it; a virtual
// clone() returns a complete copy as std::unique_ptr (C++ Core Guidelines C.67, C.130). The
// style is shared through shared_ptr<const>.
// Build: clang++ -std=c++23 -Wall -Wextra -fsanitize=address,undefined templates.cpp -o templates
#include <format>
#include <memory>
#include <print>
#include <string>
#include <utility>
#include <vector>

struct Style {
    std::string font, accent;
};

std::string count(std::size_t n) {
    return std::format("{} line{}", n, n == 1 ? "" : "s");
}

class Document {
public:
    explicit Document(std::shared_ptr<const Style> style) : style_{std::move(style)} {}
    virtual ~Document() = default;
    // public here, so the slicing below compiles; C.67 would make it protected
    Document(const Document&) = default;
    Document& operator=(const Document&) = delete;

    virtual std::string describe() const { return "document, " + count(lines.size()); }
    virtual std::unique_ptr<Document> clone() const {
        return std::make_unique<Document>(*this);
    }
    const std::shared_ptr<const Style>& style() const { return style_; }

    std::vector<std::string> lines;

private:
    std::shared_ptr<const Style> style_;
};

class Invoice final : public Document {
public:
    Invoice(std::shared_ptr<const Style> style, std::string number)
        : Document{std::move(style)}, number_{std::move(number)} {}

    std::string describe() const override {
        return "invoice " + number_ + ", " + count(lines.size());
    }
    std::unique_ptr<Document> clone() const override {
        return std::make_unique<Invoice>(*this);
    }

private:
    std::string number_;
};

int main() {
    auto house = std::make_shared<const Style>(Style{"Inter", "teal"});
    Invoice prototype{house, "INV-1042"};
    prototype.lines.push_back("kettle");
    const Document& any = prototype;

    Document sliced = any;  // copies only the Document part of the Invoice
    std::println("copied through Document&: {}", sliced.describe());

    auto copy = any.clone();  // the virtual call reaches Invoice::clone()
    copy->lines.push_back("mug");
    std::println("clone():                  {}", copy->describe());
    std::println("the prototype keeps {} line; style shared: {}", prototype.lines.size(),
                 copy->style() == prototype.style());
}

Rust · compiled/templates.rs

// Prototype in Rust: #[derive(Clone)] copies what the value owns (a Vec, a String) and shares
// what sits behind an Rc, including any RefCell inside it.
// Build: rustc --edition 2024 templates.rs
use std::cell::RefCell;
use std::rc::Rc;

struct Style {
    font: String,
}

#[derive(Clone)]
struct Template {
    style: Rc<Style>,                // shared on purpose: immutable
    lines: Vec<String>,              // owned: each clone gets its own
    notes: Rc<RefCell<Vec<String>>>, // shared and changeable: every clone sees every change
}

fn main() {
    let prototype = Template {
        style: Rc::new(Style {
            font: "Inter".to_string(),
        }),
        lines: vec!["kettle".to_string()],
        notes: Rc::new(RefCell::new(Vec::new())),
    };

    let mut copy = prototype.clone();
    copy.lines.push("mug".to_string());
    println!(
        "derive(Clone): the prototype keeps {} line; style shared: {} ({} owners of the {} style)",
        prototype.lines.len(),
        Rc::ptr_eq(&prototype.style, &copy.style),
        Rc::strong_count(&prototype.style),
        copy.style.font
    );

    copy.notes.borrow_mut().push("fragile".to_string());
    println!(
        "a RefCell behind an Rc is shared too: the prototype's notes are now {:?}",
        prototype.notes.borrow()
    );
}
All five built and run with the pinned toolchains

compiled/languages_session.py

from session import run, versions

# What each language's copy facility copies and what it shares, run with the pinned toolchains.
versions("javac --version", "node --version", "go version", "clang++ --version", "rustc --version")
run("""
java Templates.java
node templates.ts
go run templates.go
clang++ -std=c++23 -O1 -g -Wall -Wextra -fsanitize=address,undefined templates.cpp -o templates
./templates
rustc --edition 2024 -o templates_rs templates.rs && ./templates_rs
""")

Output

# javac 25.0.4.1
# v24.21.0
# go version go1.27.1 darwin/arm64
# Apple clang version 21.0.0 (clang-2100.3.34.2)
# rustc 1.99.0 (b940084d7)
$ java Templates.java
Object.clone():   the prototype now has 1 line; same list: true
copy constructor: the prototype keeps 0 lines; style shared: true
$ node templates.ts
structuredClone: lines ["kettle"]; instanceof Template: false; count(): undefined
spread copy:     the prototype now has 2 lines; style shared: true
clone():         the prototype keeps 1 line; instanceof Template: true; style shared: true
$ go run templates.go
struct assignment: the prototype's title is now "Receipt" and its first line "mug"
Clone():           the prototype keeps "Tax invoice" and 1 line; style shared: true
maps.Clone:        a slice inside the cloned map is still shared: "mug"
$ clang++ -std=c++23 -O1 -g -Wall -Wextra -fsanitize=address,undefined templates.cpp -o templates
$ ./templates
copied through Document&: document, 1 line
clone():                  invoice INV-1042, 2 lines
the prototype keeps 1 line; style shared: true
$ rustc --edition 2024 -o templates_rs templates.rs && ./templates_rs
derive(Clone): the prototype keeps 1 line; style shared: true (2 owners of the Inter style)
a RefCell behind an Rc is shared too: the prototype's notes are now ["fragile"]

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 languages_session.py

compiled/session.py

"""Prints a terminal session: "$ " and each command, then everything it printed (standard output
and standard error together), and its exit status when that is not 0.

The toolchains are the pinned ones: Java 25 (Eclipse Temurin 25.0.4), Node.js 24.21.0 and Go
1.27.1 from the mise installations next to the Python that runs this file, Rust 1.99.0 from
its rustup toolchain, and the system's Apple clang. learn:verify cannot let a sandboxed run
read the home folder those installations live in, so the sessions of this folder are recorded
with LEARN_VERIFY_NETWORK=allow. They read only this folder and the toolchains, write only
inside this folder and the run's own temporary home, and never use the network.

A command that fails and is not listed in expect_failure makes the session exit with status
1, so a run that could not build a program never becomes a record."""

import os
import pwd
import re
import signal
import subprocess
import sys
from pathlib import Path

# .../installs/python/3.14.8/bin/python3 -> .../installs
INSTALLS = Path(sys.executable).resolve().parents[3]
REAL_HOME = Path(pwd.getpwuid(os.getuid()).pw_dir)
TOOLCHAINS = [
    INSTALLS / "java" / "temurin-25.0.4+101.0.LTS" / "bin",
    INSTALLS / "node" / "24.21.0" / "bin",
    INSTALLS / "go" / "1.27.1" / "bin",
    REAL_HOME / ".rustup" / "toolchains" / "1.99.0-aarch64-apple-darwin" / "bin",
]
# Version lines that end with a build date (rustc's) are shown without it: pages show no dates.
BUILD_DATE = re.compile(r"\s+\d{4}-\d{2}-\d{2}(?=\))")


def _env() -> dict[str, str]:
    env = dict(os.environ)
    env["PATH"] = os.pathsep.join([*map(str, TOOLCHAINS), env.get("PATH", "")])
    env["GOTOOLCHAIN"] = "local"  # never download another Go
    env["GOTELEMETRY"] = "off"
    env["GOFLAGS"] = "-trimpath"
    return env


def _shell(command: str) -> subprocess.CompletedProcess[str]:
    return subprocess.run(
        command, shell=True, env=_env(), stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
    )


def versions(*commands: str) -> None:
    """Prints the first line of each version command as a comment."""
    for command in commands:
        done = _shell(command)
        lines = done.stdout.splitlines()
        if done.returncode != 0 or not lines:
            sys.exit(f"{command} did not run: the pinned toolchain is missing here")
        print("#", BUILD_DATE.sub("", lines[0]))


def run(script: str, expect_failure: tuple[str, ...] = ()) -> None:
    """Runs each line of the script as one shell command; only the commands in expect_failure
    may fail."""
    failed = 0
    for command in (line.strip() for line in script.strip().splitlines()):
        print(f"$ {command}")
        done = _shell(command)
        print(done.stdout, end="")
        if done.returncode < 0:
            print(f"[stopped by signal {signal.Signals(-done.returncode).name}]")
        elif done.returncode != 0:
            print(f"[exit status {done.returncode}]")
        if done.returncode != 0 and command not in expect_failure:
            failed += 1
    if failed:
        sys.exit(1)
  • Java. Cloneable does not contain a clone() method; its documentation says so in as many words. Object.clone() throws CloneNotSupportedException unless the class implements the interface, and otherwise fills the new object’s fields “as if by assignment”, a shallow copy: the run prints same list: true. A final list field makes it worse, because clone() runs no constructor and cannot replace a final field with a copy. A copy constructor, or a static copy factory, says field by field what is copied and what is shared.
  • TypeScript and JavaScript. structuredClone() copies the data, but the HTML standard’s algorithm recreates an ordinary object as a new plain object, so the copy is no longer a Template and has no count() method. The spread syntax { ...b } is shallow and also produces a plain object. A clone() method on the class keeps the class and copies the list.
  • Go. Assigning a struct copies its fields, and a map or slice field is only a header, so the map and the slice’s backing array are shared: changing the copy’s title changed the prototype’s. maps.Clone and slices.Clone copy one level, and their documentation calls them shallow clones: a slice stored inside a cloned map is still shared. An explicit Clone() method copies each level that can change.
  • C++. Copying an Invoice into a Document value copies only the Document part, known as slicing: the copy describes itself as a plain document. The Core Guidelines answer twice: a polymorphic class should suppress public copying (C.67), so the slicing line does not compile, and deep copies go through a virtual clone() that returns an owning pointer to the most-derived type (C.130), here std::unique_ptr<Document>.
  • Rust. #[derive(Clone)] clones each field: a Vec or String the value owns is copied, so the copy’s lines are its own. A value behind an Rc is not: cloning an Rc makes another pointer to the same allocation and raises its reference count, which is why the style is shared by both. That includes a RefCell behind an Rc, so a note pushed through the copy appears in the prototype.
The built-in copy of Python, JavaScript and Java
Criterion PythonJavaScriptJava
Shallow copy copy.copy(), list.copy()spread, Object.assign()Object.clone()
Deep copy copy.deepcopy(), with a memo for cyclesstructuredClone(), data onlyNone built in: write a copy constructor
Keeps the class YesNo, for structuredClone() and spreadYes
Trap Lists inside a shallow copyMethods and class lostShared fields, and final fields
The built-in copy of Go, C++ and Rust
Criterion GoC++Rust
Shallow copy Struct assignment, maps.Clone, slices.CloneThe copy constructor, for pointer membersCloning an Rc or Arc
Deep copy None built in: write Clone()The copy constructor, for value membersderive(Clone), for owned fields
Keeps the class YesOnly through a virtual clone()Yes
Trap Shared maps and backing arraysSlicing through a baseA RefCell behind an Rc is shared

Prototype, Factory Method and the registry

A registry of prototypes and a registry of factories solve the same problem: create an object by name without knowing its class. The difference is where the configuration lives. A factory registry holds recipes (a function that builds a configured object every time), while a prototype registry holds finished objects to copy, which means new kinds can be added while the program runs, without a new class, by configuring an object and registering it. In exchange, the copy must be correct for every kind of object in the registry, and that is the part that needs tests: change a copy, then check the prototype.

JavaScript’s own prototypes, the objects that Object.create() links a new object to, share a name with this pattern but are a language feature for inheritance: an object’s prototype supplies the properties the object itself lacks, and nothing is copied.

When not to use it

  • Construction is cheap and clear. A constructor with two arguments needs no prototype; a copy adds a second way to make the object that must be kept correct.
  • The object holds resources. Open files, sockets, database connections, locks and running threads cannot be copied meaningfully. Python’s copy module refuses to copy such types as files and sockets; in other languages a copy may even appear to work and then close the same file twice.
  • The object has an identity. A copied invoice must not keep the original’s number, and a copied user must not keep the original’s ID. Copy, then reset what identifies the object, or keep identity outside the prototype.
  • The graph is large and mostly shared. Deep-copying a 5,000-node document to change one node wastes time and memory. Immutable data with structural sharing, where a change creates a new version that reuses the unchanged parts, often beats copying.

Exercise: clone a group of shapes

Exercise · Medium · Python

Clone a group of shapes without sharing too much

A drawing program copies shapes as prototypes: select a group, press Duplicate, move the copy. Write Shape.clone() in shapes.py so that a copy is completely independent of its original, without copying what does not need copying:

- Independent lists. The copy has its own tags list and, for a Group, its own children, copied all the way down: changing a child of the copy, or the copy's list of children, never changes the original. - A shared Style. Style is immutable, so a copy and its original share the very same Style object (copy.style is original.style), at every level. You may change the Style class to make that happen, as long as it stays immutable. - Parents inside the copy. Every child of a copied group has the copied group as its parent, not the original group. The copied top shape has the same parent as the original (here None). - Any subclass. clone() is written once, in Shape, and works for Circle, Group and shape classes it has never seen: a copy has the same class and the same attributes as its original.

The sample tests check each of these points on a logo made of a group, a circle and an inner group. Run them when you are done.

Starter code · shapes.py

"""Shapes that can be grouped, and the clone() they still need."""

from dataclasses import dataclass


@dataclass(frozen=True)
class Style:
    """Immutable: shapes and their clones may share one Style."""

    stroke: str
    fill: str


class Shape:
    def __init__(self, name, style):
        self.name = name
        self.style = style
        self.tags = []  # changeable: every clone needs a list of its own
        self.parent = None  # the Group this shape belongs to, if any

    def clone(self):
        """Returns an independent copy of this shape (and, for a group, of everything in it)."""
        raise NotImplementedError("clone() is not written yet")


class Circle(Shape):
    def __init__(self, name, style, radius):
        super().__init__(name, style)
        self.radius = radius


class Group(Shape):
    def __init__(self, name, style):
        super().__init__(name, style)
        self.children = []

    def add(self, shape):
        if shape.parent is not None:
            raise ValueError(f"{shape.name} already belongs to {shape.parent.name}")
        shape.parent = self
        self.children.append(shape)
        return shape
The sample tests · test_shapes.py
from shapes import Circle, Group, Shape, Style

INK = Style(stroke="black", fill="white")
TEAL = Style(stroke="teal", fill="none")


def sample():
    """A logo: a group holding a circle and an inner group with one more circle."""
    logo = Group("logo", INK)
    logo.add(Circle("sun", TEAL, 10)).tags.append("bright")
    inner = logo.add(Group("inner", INK))
    inner.add(Circle("dot", TEAL, 2))
    return logo


def test_a_circle():
    """a circle's clone is a new Circle with the same name and radius"""
    sun = Circle("sun", TEAL, 10)
    copy = sun.clone()
    assert copy is not sun
    assert type(copy) is Circle
    assert (copy.name, copy.radius) == ("sun", 10)


def test_lists_are_independent():
    """changing the clone's children and tags never changes the original"""
    logo = sample()
    twin = logo.clone()
    twin.children[0].radius = 99
    twin.children[0].tags.append("dim")
    twin.children.pop()
    assert logo.children[0].radius == 10
    assert logo.children[0].tags == ["bright"]
    assert len(logo.children) == 2


def test_nested_groups_are_copied_too():
    """a group inside a group is copied, not shared"""
    logo = sample()
    twin = logo.clone()
    assert twin.children[1] is not logo.children[1]
    assert twin.children[1].children[0] is not logo.children[1].children[0]
    assert twin.children[1].children[0].radius == 2


def test_style_is_shared():
    """the immutable Style is shared by identity, at every level"""
    logo = sample()
    twin = logo.clone()
    assert twin.style is logo.style
    assert twin.children[0].style is logo.children[0].style
    assert twin.children[1].children[0].style is TEAL


def test_parents_point_into_the_clone():
    """every copied child's parent is the copied group, not the original one"""
    logo = sample()
    twin = logo.clone()
    assert twin.parent is None
    assert all(child.parent is twin for child in twin.children)
    inner = twin.children[1]
    assert inner.children[0].parent is inner


def test_a_subclass_survives():
    """a Shape subclass that clone() has never seen keeps its class and its own attributes"""

    class Label(Shape):
        def __init__(self, name, style, text):
            super().__init__(name, style)
            self.text = text

    group = Group("caption", INK)
    group.add(Label("title", INK, "Order 1042"))
    twin = group.clone()
    assert type(twin.children[0]) is Label
    assert twin.children[0].text == "Order 1042"
    assert twin.children[0].parent is twin


def test_a_shape_joins_one_group():
    """add() still refuses a shape that already belongs to a group"""
    logo = sample()
    try:
        Group("other", INK).add(logo.children[0])
    except ValueError:
        pass
    else:
        raise AssertionError("a shape that belongs to a group cannot be added to another one")


def test_style_stays_immutable():
    """Style is still immutable, which is what makes sharing it safe"""
    try:
        INK.stroke = "red"
    except AttributeError:
        pass
    else:
        raise AssertionError("a Style must not be changeable: sharing it is safe only because it is immutable")
A hint

Check what copy.deepcopy already does with nested lists, with the class of each object, and with a reference back to an object it has already copied in the same call (a child's parent). The one thing it cannot know is which objects are safe to share; the copy module's documentation shows how a class can say how it should be copied.

The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.

JSON Diff & Compare Print a prototype and its copy as JSON and compare them to see exactly what changed.

Check yourself

Check yourself

5 questions about this lesson. Every answer and why it is right is on the page, behind “Show the answer”. Your score stays in this browser.

  1. Question 1 of 5 What does this print?

    Read the code, then choose one answer.

    import copy
    
    template = {"title": "Receipt", "lines": ["kettle"]}
    a = copy.copy(template)
    b = copy.deepcopy(template)
    a["lines"].append("mug")
    b["lines"].append("tray")
    a["title"] = "Invoice"
    print(template)
    Show the answer to question 1

    Answer: {'title': 'Receipt', 'lines': ['kettle', 'mug']}

    The shallow copy a is a new dictionary that refers to the same list, so appending through it changes the template's list; assigning a["title"] only rebinds a key of a. The deep copy b has a list of its own, so "tray" never reaches the template.

  2. Question 2 of 5 In Node.js, what is structuredClone(new Template(style, ["kettle"]))?

    Choose one answer.

    Show the answer to question 2

    Answer: A plain object with copies of the data properties, which is not an instance of Template and has none of its methods

    The structured clone algorithm of the HTML standard serialises an ordinary object's own properties and recreates it as a new plain object, so the class is lost: instanceof Template is false and count() is undefined, as the recorded run shows. An explicit clone() method keeps the class.

  3. Question 3 of 5 Which statement about Java's Cloneable is true?

    Choose one answer.

    Show the answer to question 3

    Answer: It contains no clone() method; Object.clone() makes a field-by-field shallow copy and throws CloneNotSupportedException for classes that do not implement Cloneable

    The interface's documentation says that it does not contain the clone method. Object.clone() copies every field as if by assignment, so a list field is shared, as Templates.java prints. A copy constructor or a static copy factory says field by field what is copied.

  4. Question 4 of 5 In C++, Document sliced = any; copies an Invoice through a const Document&. What does sliced.describe() return, if describe() is virtual?

    Choose one answer.

    Show the answer to question 4

    Answer: The Document's description, because only the Document part was copied

    Copying into a Document value copies only the Document part, so the object really is a Document and the virtual call goes to Document::describe(). A virtual clone() that returns std::unique_ptr<Document> copies the most-derived object (C++ Core Guidelines C.130), and making the base's copy constructor protected turns the slicing line into a compile error (C.67).

  5. Question 5 of 5 Which of these Go operations share data with the original after the copy?

    Choose every answer that is right.

    Show the answer to question 5

    Answer:

    • Assigning a struct that has a map field, shallow := *p
    • maps.Clone(m) when the values of m are slices

    Struct assignment copies a map header, so both structs see one map, and maps.Clone copies the map but not the slices stored in it ("a shallow clone", in its documentation). Cloning a slice of integers, or copying a struct of plain values, leaves nothing shared.

Interview questions

Fresher · What is the difference between a shallow copy and a deep copy?

A shallow copy makes a new outer object but fills it with references to the same inner objects as the original, so a list or dictionary inside is shared: appending through the copy changes the original. A deep copy makes the outer object and, recursively, copies of the inner objects, so the two share nothing that can change. The difference only matters for compound objects, those that contain other mutable objects. In Python they are copy.copy and copy.deepcopy, and deepcopy uses a memo dictionary so that shared references and cycles are copied once. A good answer adds that a deep copy may still share immutable parts on purpose, which saves memory, and that objects such as open files or sockets cannot be deep-copied meaningfully at all.

Key takeaways

  • Prototype creates objects by copying configured instances; a registry of prototypes lets new kinds be added at run time without new classes.
  • The design decision is what a copy shares: copy everything that can change, and share what is immutable.
  • copy.copy shares inner lists and copy.deepcopy copies them, keeping a memo for cycles; a class controls its own copying with __deepcopy__.
  • Java’s Cloneable has no clone() and Object.clone() is shallow; structuredClone loses the class; Go’s struct assignment and maps.Clone share inner data; C++ slices through a base; Rust’s Rc shares on clone.
  • Do not copy objects that hold resources or carry an identity without resetting it, and prefer immutable data for large graphs.

References

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.