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.
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
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
Stylethat 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
"""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
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
Line by line:
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.copy.copy()makes a shallow copy, and appending to itslineschanged the prototype: the bug this pattern is known for.clone()usescopy.deepcopy(), so the copy owns its lists.Styledefines__deepcopy__()to return itself, which is how a class tellsdeepcopythat it may be shared: the copy’s style is the prototype’s style.- 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.
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, ©.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()
);
} 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.
Cloneabledoes not contain aclone()method; its documentation says so in as many words.Object.clone()throwsCloneNotSupportedExceptionunless the class implements the interface, and otherwise fills the new object’s fields “as if by assignment”, a shallow copy: the run printssame list: true. Afinallist field makes it worse, becauseclone()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 aTemplateand has nocount()method. The spread syntax{ ...b }is shallow and also produces a plain object. Aclone()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.Cloneandslices.Clonecopy one level, and their documentation calls them shallow clones: a slice stored inside a cloned map is still shared. An explicitClone()method copies each level that can change. - C++. Copying an
Invoiceinto aDocumentvalue copies only theDocumentpart, 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 virtualclone()that returns an owning pointer to the most-derived type (C.130), herestd::unique_ptr<Document>. - Rust.
#[derive(Clone)]clones each field: aVecorStringthe value owns is copied, so the copy’s lines are its own. A value behind anRcis not: cloning anRcmakes another pointer to the same allocation and raises its reference count, which is why the style is shared by both. That includes aRefCellbehind anRc, so a note pushed through the copy appears in the prototype.
| Criterion | Python | JavaScript | Java |
|---|---|---|---|
| Shallow copy | copy.copy(), list.copy() | spread, Object.assign() | Object.clone() |
| Deep copy | copy.deepcopy(), with a memo for cycles | structuredClone(), data only | None built in: write a copy constructor |
| Keeps the class | Yes | No, for structuredClone() and spread | Yes |
| Trap | Lists inside a shallow copy | Methods and class lost | Shared fields, and final fields |
| Criterion | Go | C++ | Rust |
|---|---|---|---|
| Shallow copy | Struct assignment, maps.Clone, slices.Clone | The copy constructor, for pointer members | Cloning an Rc or Arc |
| Deep copy | None built in: write Clone() | The copy constructor, for value members | derive(Clone), for owned fields |
| Keeps the class | Yes | Only through a virtual clone() | Yes |
| Trap | Shared maps and backing arrays | Slicing through a base | A 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
copymodule 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.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
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.
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.
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.copyshares inner lists andcopy.deepcopycopies them, keeping a memo for cycles; a class controls its own copying with__deepcopy__.- Java’s
Cloneablehas noclone()andObject.clone()is shallow;structuredCloneloses the class; Go’s struct assignment andmaps.Cloneshare inner data; C++ slices through a base; Rust’sRcshares on clone. - Do not copy objects that hold resources or carry an identity without resetting it, and prefer immutable data for large graphs.
References
- Design Patterns: Elements of Reusable Object-Oriented Software (Prototype) (Addison-Wesley (Pearson))
- copy: shallow and deep copy operations (Python Software Foundation)
- HTML Standard: safe passing of structured data (structured clone) (WHATWG)
- Cloneable (Java SE 25) (Oracle)
- Object.clone() (Java SE 25) (Oracle)
- Package maps: Clone (The Go Authors)
- Package slices: Clone (The Go Authors)
- C++ Core Guidelines: C.67 and C.130 (Standard C++ Foundation)
- The Clone trait (Rust standard library) (The Rust Project)
- Rc (Rust standard library) (The Rust Project)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress