Skip to main content

Python Shallow vs Deep Copy Practice Problems & Exercises

Practice: Shallow vs Deep Copy

11 problems4 Easy4 Medium3 Hard40-55 min
← Back to lesson

Easy

#1Assignment vs Shallow CopyEasy
assignmentaliasingshallow-copyidentity

Predict the output of each print statement. Pay attention to the difference between assignment (aliasing) and shallow copy for a flat list of integers.

Python
original = [1, 2, 3]
alias = original           # assignment — alias or copy?
shallow = original.copy()  # .copy() — alias or copy?

# Identity checks
print(f"alias is original: {alias is original}")
print(f"shallow is original: {shallow is original}")
print(f"alias[0] is original[0]: {alias[0] is original[0]}")
print(f"shallow[0] is original[0]: {shallow[0] is original[0]}")

# Mutation via alias
alias.append(4)
print("After alias.append(4):")
print(f"  original = {original}")
print(f"  alias    = {alias}")
print(f"  shallow  = {shallow}")

# Mutation via shallow copy
shallow.append(99)
print("After shallow.append(99):")
print(f"  original = {original}")
print(f"  shallow  = {shallow}")
Solution
alias is original: True
shallow is original: False
alias[0] is original[0]: True
shallow[0] is original[0]: True
After alias.append(4):
original = [1, 2, 3, 4]
alias = [1, 2, 3, 4]
shallow = [1, 2, 3]
After shallow.append(99):
original = [1, 2, 3, 4]
shallow = [1, 2, 3, 99]

Key distinctions:

  • alias = original creates an alias. Both names point to the same list object. alias is original is True. Any mutation through either name affects the one shared list.
  • shallow = original.copy() creates a new list object with the same elements. shallow is original is False. Appending to shallow does not affect original because you are modifying the container, not the shared elements.
  • Even though shallow[0] is original[0] is True (the integer objects are shared), this is safe because integers are immutable. You cannot mutate 1 in-place.
Expected Output
alias is original: True
shallow is original: False
alias[0] is original[0]: True
shallow[0] is original[0]: True
After alias.append(4):
  original = [1, 2, 3, 4]
  alias    = [1, 2, 3, 4]
  shallow  = [1, 2, 3]
After shallow.append(99):
  original = [1, 2, 3, 4]
  shallow  = [1, 2, 3, 99]
Hints

Hint 1: Assignment (`=`) creates an alias — both names point to the same object. `id(alias) == id(original)`.

Hint 2: `.copy()` creates a new outer list, but elements inside are still shared references. Appending to the copy does not affect the original because you are modifying the container, not the elements.

#2Shallow Copy and Nested MutationEasy
shallow-copynested-listmutationshared-reference

Predict the output. A shallow copy of a nested list is created. Two different mutations are performed — one safe, one dangerous.

Python
import copy

a = [1, [2, 3], 4]
b = copy.copy(a)

# Mutation 1: rebind an element in b
b[0] = 99
print("After b[0] = 99:")
print(f"  a = {a}")
print(f"  b = {b}")

# Mutation 2: mutate a shared nested object
b[1].append(99)
print("After b[1].append(99):")
print(f"  a = {a}")
print(f"  b = {b}")
Solution
After b[0] = 99:
a = [1, [2, 3], 4]
b = [99, [2, 3], 4]
After b[1].append(99):
a = [1, [2, 3, 99], 4]
b = [99, [2, 3, 99], 4]

Two types of "modification":

  1. Rebinding (b[0] = 99): This changes which object slot 0 of b points to. It replaces the pointer in b, not the integer 1 itself. Since a and b are different containers, a[0] is unaffected.

  2. Mutating (b[1].append(99)): This calls .append() on the inner list object that both a[1] and b[1] point to. There is only one inner list — both a and b share a reference to it. So the mutation is visible through both names.

This is the core danger of shallow copy with nested mutable objects.

Expected Output
After b[0] = 99:
  a = [1, [2, 3], 4]
  b = [99, [2, 3], 4]
After b[1].append(99):
  a = [1, [2, 3, 99], 4]
  b = [99, [2, 3, 99], 4]
Hints

Hint 1: Shallow copy creates a new outer list but the inner `[2, 3]` list is the same object in both `a` and `b`.

Hint 2: `b[0] = 99` rebinds the first slot of `b` to a new integer — this does NOT affect `a[0]`. But `b[1].append(99)` mutates the shared inner list, which IS visible through `a[1]`.

#3Four Ways to Shallow Copy a ListEasy
shallow-copylist-copysliceconstructorcopy-module

Verify that all four shallow copy methods produce equivalent results — new outer containers but shared inner references.

Python
import copy

original = [[1, 2], [3, 4]]

c1 = original.copy()       # list method
c2 = original[:]           # slice syntax
c3 = list(original)        # list constructor
c4 = copy.copy(original)   # copy module

# Check: all are different outer objects
all_different = (
    c1 is not original and
    c2 is not original and
    c3 is not original and
    c4 is not original
)
print(f"All are different objects from original: {all_different}")

# Check: all share inner lists
all_shared = (
    c1[0] is original[0] and
    c2[0] is original[0] and
    c3[0] is original[0] and
    c4[0] is original[0]
)
print(f"All share inner lists with original: {all_shared}")

# Demonstrate the shared reference danger
c1[0].append(999)
print(f"After mutating via c1, original inner = {original[0]}")
Solution
All are different objects from original: True
All share inner lists with original: True
After mutating via c1, original inner = [1, 2, 999]

All four methods are equivalent shallow copies:

MethodNew outer?Inner shared?
.copy()YesYes
[:]YesYes
list()YesYes
copy.copy()YesYes

They all create a new list object and copy the references (pointers) to the inner objects. None of them recurse into nested objects. The only way to get fully independent nested copies is copy.deepcopy().

Appending to c1[0] mutates the same list object that original[0], c2[0], c3[0], and c4[0] all point to.

Expected Output
All are different objects from original: True
All share inner lists with original: True
After mutating via c1, original inner = [1, 2, 999]
Hints

Hint 1: There are four equivalent ways to shallow-copy a list: `list.copy()`, `list[:]`, `list(original)`, and `copy.copy(original)`. All produce the same result.

Hint 2: Since all four are shallow copies, every one of them shares the same inner list objects with the original.

#4Dict Shallow Copy TrapEasy
dict-copyshallow-copynested-dictconfiguration

Predict the output. This simulates the classic configuration system bug caused by using dict.copy() on a nested dictionary.

Python
default_config = {
    "database": {"host": "localhost", "port": 5432},
    "timeout": 30,
}

prod_config = default_config.copy()

# Modify nested dict value
prod_config["database"]["host"] = "prod-db.internal"

# Modify top-level value
prod_config["timeout"] = 60

print(f"default db host: {default_config['database']['host']}")
print(f"default timeout: {default_config['timeout']}")

if default_config["database"]["host"] != "localhost":
    print("BUG: default was mutated by shallow copy!")
else:
    print("Safe: default was not mutated.")
Solution
default db host: prod-db.internal
default timeout: 30
BUG: default was mutated by shallow copy!

Why this happens:

  • default_config.copy() creates a new outer dict, but the value for "database" is a reference to the same inner dict object.
  • prod_config["database"]["host"] = "prod-db.internal" modifies the inner dict through the shared reference. Both default_config["database"] and prod_config["database"] point to the same dict, so the change is visible in both.
  • prod_config["timeout"] = 60 is safe because it rebinds a key in the new outer dict. default_config["timeout"] still points to the integer 30.

The fix: Use copy.deepcopy(default_config) to create a fully independent copy of the entire nested structure.

Expected Output
default db host: prod-db.internal
default timeout: 30
BUG: default was mutated by shallow copy!
Hints

Hint 1: `dict.copy()` creates a new dict but does NOT copy nested dicts. The inner `"database"` dict is shared between `default_config` and `prod_config`.

Hint 2: Modifying `prod_config["database"]["host"]` mutates the shared inner dict. But `prod_config["timeout"] = 60` only rebinds a key in the new outer dict.


Medium

#5Deep Copy Independence VerifierMedium
deepcopynested-structureidentityindependence

Build a 4-level nested structure and verify that deepcopy creates independent objects at every level. Check identity (is) at each depth and prove mutation isolation.

Python
import copy

# 4-level nested structure
original = {
    "level1": {
        "level2": {
            "level3": [10, 20]
        }
    }
}

deep = copy.deepcopy(original)

# Verify identity at each level
outer_diff = deep is not original
l1_diff = deep["level1"] is not original["level1"]
l2_diff = deep["level1"]["level2"] is not original["level1"]["level2"]
l3_diff = deep["level1"]["level2"]["level3"] is not original["level1"]["level2"]["level3"]

print(f"Outer identity different: {outer_diff}")
print(f"Level-1 identity different: {l1_diff}")
print(f"Level-2 identity different: {l2_diff}")
print(f"Level-3 identity different: {l3_diff}")

# Mutate the deepest level in the copy
deep["level1"]["level2"]["level3"].append(999)

orig_l3 = original["level1"]["level2"]["level3"]
deep_l3 = deep["level1"]["level2"]["level3"]

print("After deep mutation:")
print(f"  original level-3 = {orig_l3}")
print(f"  deep level-3     = {deep_l3}")

independence = orig_l3 == [10, 20] and deep_l3 == [10, 20, 999]
print(f"Full independence verified: {independence}")
Solution
Outer identity different: True
Level-1 identity different: True
Level-2 identity different: True
Level-3 identity different: True
After deep mutation:
original level-3 = [10, 20]
deep level-3 = [10, 20, 999]
Full independence verified: True

What deepcopy does internally:

  1. Creates a new outer dict. Registers it in the memo dict.
  2. For each key-value pair, recursively deep-copies the value.
  3. The "level1" value is a dict — create a new dict, recurse into its values.
  4. Continue recursing through "level2" and "level3" until reaching immutable leaf values (integers 10, 20).
  5. The result: every mutable container at every depth is a brand-new object.

At the leaf level, immutable objects like integers may or may not be shared (CPython often reuses small integers), but this is harmless because they cannot be mutated.

Expected Output
Outer identity different: True
Level-1 identity different: True
Level-2 identity different: True
Level-3 identity different: True
After deep mutation:
  original level-3 = [10, 20]
  deep level-3     = [10, 20, 999]
Full independence verified: True
Hints

Hint 1: `copy.deepcopy()` recursively copies every object at every depth. Use `is` to verify that each level is a different object.

Hint 2: After deep copy, mutating any level of the copy — no matter how deeply nested — should never affect the original.

#6Safe Test Fixture FactoryMedium
deepcopytest-fixturesfactory-patternisolation

Implement a make_fixture function that returns an independent copy of a base template. Prove that modifying one fixture does not affect the template or other fixtures.

Python
import copy

CART_TEMPLATE = {
    "items": [{"id": "A", "qty": 2}],
    "discounts": [],
    "total": 0,
}

def make_fixture():
    """Return a fully independent copy of the cart template."""
    return copy.deepcopy(CART_TEMPLATE)

# Simulate two tests getting their own fixtures
fixture1 = make_fixture()
fixture2 = make_fixture()

# Test 1 modifies its fixture
fixture1["items"].append({"id": "B", "qty": 1})
fixture1["discounts"].append("SAVE10")
fixture1["total"] = 29.99

# Verify isolation
print(f"fixture1 items: {fixture1['items']}")
print(f"fixture2 items: {fixture2['items']}")
print(f"template items: {CART_TEMPLATE['items']}")

isolated = (
    len(fixture1["items"]) == 2 and
    len(fixture2["items"]) == 1 and
    len(CART_TEMPLATE["items"]) == 1 and
    len(fixture1["discounts"]) == 1 and
    len(fixture2["discounts"]) == 0 and
    len(CART_TEMPLATE["discounts"]) == 0
)
print(f"All isolated: {isolated}")
Solution
fixture1 items: [{'id': 'A', 'qty': 2}, {'id': 'B', 'qty': 1}]
fixture2 items: [{'id': 'A', 'qty': 2}]
template items: [{'id': 'A', 'qty': 2}]
All isolated: True

Why deepcopy is essential here:

If you used CART_TEMPLATE.copy() (shallow copy), the "items" list and "discounts" list would be shared between all fixtures. Appending to fixture1["items"] would modify the template and every other fixture created from it.

This is a real-world pattern used in test suites:

class TestCheckout(unittest.TestCase):
BASE_CART = {"items": [...], "discounts": []}

def setUp(self):
self.cart = copy.deepcopy(self.BASE_CART)

Each test method gets a completely independent cart. No test can pollute another test's state.

Expected Output
fixture1 items: [{'id': 'A', 'qty': 2}, {'id': 'B', 'qty': 1}]
fixture2 items: [{'id': 'A', 'qty': 2}]
template items: [{'id': 'A', 'qty': 2}]
All isolated: True
Hints

Hint 1: A test fixture factory should return `copy.deepcopy()` of the template so each test gets a fully independent copy.

Hint 2: Verify isolation by mutating one fixture and checking that the template and other fixtures are unaffected.

#7Mutation-Safe Function ParametersMedium
defensive-copyfunction-parametersmutation-safetyapi-design

Implement process_data so that it adds a zero-padding value to "values", adds a "processed" flag to "metadata", and computes the mean — all without mutating the input.

Python
import copy

def process_data(data):
    """Process data without mutating the input.

    Adds a 0 to values, sets metadata['processed'] = True,
    and adds a 'mean' key with the average of original values.
    """
    result = copy.deepcopy(data)
    original_values = data["values"]  # for computing mean from original
    result["values"].append(0)
    result["metadata"]["processed"] = True
    result["mean"] = sum(original_values) / len(original_values)
    return result

# Test
original = {
    "values": [1, 2, 3],
    "metadata": {"source": "sensor"},
}

result = process_data(original)

print("After process_data:")
print(f"  original = {original}")
print(f"  result   = {result}")

unchanged = (
    original["values"] == [1, 2, 3] and
    "processed" not in original["metadata"] and
    "mean" not in original
)
print(f"Original unchanged: {unchanged}")
Solution
After process_data:
original = {'values': [1, 2, 3], 'metadata': {'source': 'sensor'}}
result = {'values': [1, 2, 3, 0], 'metadata': {'source': 'sensor', 'processed': True}, 'mean': 1.5}
Original unchanged: True

The defensive copy pattern:

  1. Deep copy the input at the top of the function.
  2. Perform all mutations on the copy.
  3. Return the modified copy.

This is essential when:

  • The caller does not expect their data to be modified.
  • The same data is passed to multiple functions.
  • The function is part of a pipeline where each stage should not corrupt the shared input.

Without the deep copy, result["values"].append(0) and result["metadata"]["processed"] = True would mutate the caller's original dict because a shallow copy would share the nested "values" list and "metadata" dict.

Expected Output
After process_data:
  original = {'values': [1, 2, 3], 'metadata': {'source': 'sensor'}}
  result   = {'values': [1, 2, 3, 0], 'metadata': {'source': 'sensor', 'processed': True}, 'mean': 1.5}
Original unchanged: True
Hints

Hint 1: A function that needs to modify its input should deep-copy the argument first and work on the copy.

Hint 2: Return the modified copy. The caller's original data remains untouched — this is the defensive copying pattern.

#8Shallow Copy Safety CheckerMedium
shallow-copyimmutable-leaf-ruletype-checkingintrospection

Implement is_shallow_safe that returns True if a shallow copy of the given container would be safe (no shared mutable nested objects). Only check one level deep.

Python
def is_shallow_safe(container):
    """Return True if shallow copy of container is safe for independent mutation.

    Safe means: all elements (list) or values (dict) are immutable types.
    """
    immutable_types = (int, float, str, bool, type(None), tuple, frozenset, bytes)

    if isinstance(container, list):
        return all(isinstance(item, immutable_types) for item in container)
    elif isinstance(container, dict):
        return all(isinstance(v, immutable_types) for v in container.values())
    else:
        return True  # Non-container types are trivially safe

# Test cases
test_cases = [
    [1, 2, 3],
    ["a", "b", "c"],
    [(1, 2), (3, 4)],
    [[1, 2], [3, 4]],
    {"a": 1, "b": 2},
    {"a": [1, 2]},
    {"a": {"b": 1}},
    [1, "two", (3,), True, None],
    [1, "two", [3], True],
]

for tc in test_cases:
    safe = is_shallow_safe(tc)
    label = repr(tc)
    print(f"{label:33s}-> shallow safe: {safe}")
Solution
[1, 2, 3] -> shallow safe: True
['a', 'b', 'c'] -> shallow safe: True
[(1, 2), (3, 4)] -> shallow safe: True
[[1, 2], [3, 4]] -> shallow safe: False
{'a': 1, 'b': 2} -> shallow safe: True
{'a': [1, 2]} -> shallow safe: False
{'a': {'b': 1}} -> shallow safe: False
[1, 'two', (3,), True, None] -> shallow safe: True
[1, 'two', [3], True] -> shallow safe: False

The Immutable Leaf Rule:

Shallow copy is safe if and only if every element at the first level of nesting is immutable. The immutable built-in types are: int, float, str, bool, NoneType, tuple, frozenset, bytes.

Edge case: Tuples containing mutable objects (e.g., ([1, 2],)) are technically mutable-by-proxy — the tuple itself cannot be reassigned, but its contents can be mutated. A more thorough checker would recurse into tuples, but for practical purposes, checking one level is usually sufficient.

When to use this rule: Before choosing between copy.copy() and copy.deepcopy(), mentally (or programmatically) check whether your data contains nested mutable objects. If it does, use deepcopy.

Expected Output
[1, 2, 3]                    -> shallow safe: True
['a', 'b', 'c']              -> shallow safe: True
[(1, 2), (3, 4)]             -> shallow safe: True
[[1, 2], [3, 4]]             -> shallow safe: False
{'a': 1, 'b': 2}             -> shallow safe: True
{'a': [1, 2]}                -> shallow safe: False
{'a': {'b': 1}}              -> shallow safe: False
[1, 'two', (3,), True, None] -> shallow safe: True
[1, 'two', [3], True]        -> shallow safe: False
Hints

Hint 1: Shallow copy is safe when all elements (for lists) or all values (for dicts) are immutable types: int, float, str, bool, NoneType, tuple, frozenset, bytes.

Hint 2: Check each element's type. If any element is a mutable type (list, dict, set, or a custom mutable object), shallow copy is NOT safe for independent mutation.


Hard

#9Custom __copy__ and __deepcopy__Hard
__copy____deepcopy__custom-protocolmemo-dictcopy-control

Implement __copy__ and __deepcopy__ on a ServerConfig class. The shallow copy should share the tags list. The deep copy should produce a fully independent clone with optimized handling of immutable fields.

Python
import copy

class ServerConfig:
    def __init__(self, name, port, tags):
        self.name = name    # str (immutable)
        self.port = port    # int (immutable)
        self.tags = tags    # list (mutable)

    def __copy__(self):
        new = ServerConfig.__new__(ServerConfig)
        new.name = self.name
        new.port = self.port
        new.tags = self.tags  # shallow: share the list
        return new

    def __deepcopy__(self, memo):
        new = ServerConfig.__new__(ServerConfig)
        memo[id(self)] = new  # register BEFORE recursing
        new.name = self.name  # str is immutable — safe to share
        new.port = self.port  # int is immutable — safe to share
        new.tags = copy.deepcopy(self.tags, memo)  # list must be deep copied
        return new

# Test shallow copy
original = ServerConfig("localhost", 5432, ["primary", "write"])
shallow = copy.copy(original)

print("--- Shallow copy ---")
print(f"shallow.name: {shallow.name} (same object: {shallow.name is original.name})")
print(f"shallow.port: {shallow.port} (same object: {shallow.port is original.port})")
print(f"shallow.tags: {shallow.tags} (same object: {shallow.tags is original.tags})")
shallow.tags.append("danger")
original.tags.pop()  # remove "danger"
print(f"shallow.tags mutation affects original: {shallow.tags is original.tags}")

# Test deep copy
original2 = ServerConfig("localhost", 5432, ["primary", "write"])
deep = copy.deepcopy(original2)

print("--- Deep copy ---")
print(f"deep.name: {deep.name} (same object: {deep.name is original2.name})")
print(f"deep.port: {deep.port} (same object: {deep.port is original2.port})")
print(f"deep.tags: {deep.tags} (same object: {deep.tags is original2.tags})")

deep.tags.append("replica")
print("After deep.tags.append:")
print(f"  original tags: {original2.tags}")
print(f"  deep tags:     {deep.tags}")
print(f"Deep copy isolated: {original2.tags == ['primary', 'write']}")
Solution
--- Shallow copy ---
shallow.name: localhost (same object: True)
shallow.port: 5432 (same object: True)
shallow.tags: ['primary', 'write'] (same object: True)
shallow.tags mutation affects original: True
--- Deep copy ---
deep.name: localhost (same object: True)
deep.port: 5432 (same object: True)
deep.tags: ['primary', 'write'] (same object: False)
After deep.tags.append:
original tags: ['primary', 'write']
deep tags: ['primary', 'write', 'replica']
Deep copy isolated: True

Key implementation details:

  1. __copy__ creates a new ServerConfig but shares all attribute references. For mutable attributes like tags, this means mutations through the copy affect the original.

  2. __deepcopy__ must follow a strict protocol:

    • Use __new__ to create the instance without calling __init__ (avoids side effects).
    • Register memo[id(self)] = new before recursing into attributes. This prevents infinite recursion if the object graph has circular references.
    • Pass memo to every copy.deepcopy() call so the entire graph shares one memo dict.
    • Optimization: immutable fields (str, int) are assigned directly instead of deep-copied.
  3. Why name is original.name is True for deep copy: Strings are immutable and CPython interns them. Our optimized __deepcopy__ deliberately shares immutable fields. Even if you called copy.deepcopy(self.name, memo), CPython would likely return the same string object.

Expected Output
--- Shallow copy ---
shallow.name: localhost (same object: True)
shallow.port: 5432 (same object: True)
shallow.tags: ['primary', 'write'] (same object: True)
shallow.tags mutation affects original: True
--- Deep copy ---
deep.name: localhost (same object: True)
deep.port: 5432 (same object: True)
deep.tags: ['primary', 'write'] (same object: False)
After deep.tags.append:
  original tags: ['primary', 'write']
  deep tags:     ['primary', 'write', 'replica']
Deep copy isolated: True
Hints

Hint 1: `__copy__` should create a new instance and copy all attributes. For shallow copy, mutable attributes like lists are shared (same reference).

Hint 2: `__deepcopy__` must: (1) create new instance via `__new__`, (2) register in `memo` with `memo[id(self)] = new_obj` BEFORE recursing, (3) use `copy.deepcopy(attr, memo)` for mutable fields. Immutable fields like `str` and `int` can be assigned directly as an optimization.

#10Deep Copy with Circular ReferencesHard
deepcopycircular-referencememo-dictobject-graph

Create a circular object graph and verify that deepcopy handles it correctly — reproducing the circular structure with new, independent objects.

Python
import copy

class Node:
    def __init__(self, name, neighbor=None):
        self.name = name
        self.neighbor = neighbor

# Build circular reference: A -> B -> A
node_a = Node("A")
node_b = Node("B")
node_a.neighbor = node_b
node_b.neighbor = node_a

# Verify original circularity
print(f"Original circular: node_a.neighbor.neighbor is node_a -> {node_a.neighbor.neighbor is node_a}")

# Deep copy the circular graph
deep_a = copy.deepcopy(node_a)

# Verify deep copy has its own circular structure
print(f"Deep copy circular: deep_a.neighbor.neighbor is deep_a -> {deep_a.neighbor.neighbor is deep_a}")

# Verify independence from original
print(f"Independence: deep_a is not node_a -> {deep_a is not node_a}")
print(f"Independence: deep_a.neighbor is not node_b -> {deep_a.neighbor is not node_b}")

# Verify mutation isolation
deep_a.name = "A-modified"
print("After mutating deep copy:")
print(f"  original name: {node_a.name}")
print(f"  deep copy name: {deep_a.name}")

# The memo dict tracked 2 objects (node_a and node_b)
print(f"Memo prevented infinite recursion: {deep_a.neighbor.neighbor.neighbor.neighbor is deep_a}")
Solution
Original circular: node_a.neighbor.neighbor is node_a -> True
Deep copy circular: deep_a.neighbor.neighbor is deep_a -> True
Independence: deep_a is not node_a -> True
Independence: deep_a.neighbor is not node_b -> True
After mutating deep copy:
original name: A
deep copy name: A-modified
Memo prevented infinite recursion: True

How deepcopy handles the circular reference:

Step 1: deepcopy(node_a)
memo = {}
id(node_a) not in memo -> create deep_a = Node.__new__(Node)
memo = {id(node_a): deep_a}

Step 2: deepcopy(node_a.name) -> "A" (str, immutable, returned as-is)
deep_a.name = "A"

Step 3: deepcopy(node_a.neighbor) = deepcopy(node_b)
id(node_b) not in memo -> create deep_b = Node.__new__(Node)
memo = {id(node_a): deep_a, id(node_b): deep_b}

Step 4: deepcopy(node_b.name) -> "B"
deep_b.name = "B"

Step 5: deepcopy(node_b.neighbor) = deepcopy(node_a)
id(node_a) IS in memo -> return memo[id(node_a)] = deep_a
deep_b.neighbor = deep_a (CIRCULAR LINK REPRODUCED!)

Step 6: deep_a.neighbor = deep_b

Result: deep_a -> deep_b -> deep_a (circular, fully independent from originals)

The memo dict is the key mechanism. Without it, Step 5 would recursively call deepcopy(node_a) again, which would call deepcopy(node_b), which would call deepcopy(node_a) — infinite recursion.

Expected Output
Original circular: node_a.neighbor.neighbor is node_a -> True
Deep copy circular: deep_a.neighbor.neighbor is deep_a -> True
Independence: deep_a is not node_a -> True
Independence: deep_a.neighbor is not node_b -> True
After mutating deep copy:
  original name: A
  deep copy name: A-modified
Memo prevented infinite recursion: True
Hints

Hint 1: Create two Node objects that reference each other: `a.neighbor = b` and `b.neighbor = a`. This forms a circular reference.

Hint 2: `copy.deepcopy()` handles this automatically via the `memo` dict. The deep copy reproduces the circular structure with new objects — `deep_a.neighbor.neighbor is deep_a` should be `True`.

#11Copy-on-Write Template CacheHard
deepcopydefensive-copycachecopy-on-writeapi-design

Implement a TemplateCache class with full copy-on-write semantics. The cache must be immune to mutation from any external reference — both the input to add/update and the output from get.

Python
import copy

class TemplateCache:
    def __init__(self):
        self._store = {}

    def add(self, name, template):
        """Store a deep copy — caller's reference cannot corrupt cache."""
        self._store[name] = copy.deepcopy(template)

    def get(self, name):
        """Return a deep copy — caller can mutate freely."""
        if name not in self._store:
            raise KeyError(f"No template: {name}")
        return copy.deepcopy(self._store[name])

    def update(self, name, template):
        """Replace with a deep copy of the new template."""
        self._store[name] = copy.deepcopy(template)

cache = TemplateCache()

# --- Basic isolation ---
print("--- Basic isolation ---")
cache.add("api", {
    "headers": {"Content-Type": "application/json"},
    "retry": {"max": 3, "delays": [1, 2, 4]},
})

caller = cache.get("api")
caller["headers"]["Authorization"] = "Bearer secret"

stored = cache.get("api")
print(f"Template after caller mutation: {stored}")
print(f"Caller copy has extra header: {'Authorization' in caller['headers']}")

# --- Input isolation ---
print("--- Input isolation ---")
raw = [1, 2, 3]
cache.add("raw", raw)
raw.append(999)  # mutate original after adding
print(f"Template after raw mutation: {cache.get('raw')}")

# --- Multiple callers ---
print("--- Multiple callers ---")
caller1 = cache.get("api")
caller2 = cache.get("api")

caller1["retry"]["delays"].append(8)

print(f"caller1 delays: {caller1['retry']['delays']}")
print(f"caller2 delays: {caller2['retry']['delays']}")

template_delays = cache.get("api")["retry"]["delays"]
print(f"template delays: {template_delays}")
print(f"All isolated: {caller1['retry']['delays'] != caller2['retry']['delays']}")

# --- Update isolation ---
print("--- Update isolation ---")
new_retry = {"max": 5, "delays": [1, 2]}
cache.update("api", {"headers": {"Content-Type": "application/json"}, "retry": new_retry})
new_retry["delays"].append(999)  # mutate after update

updated = cache.get("api")
print(f"Template after update: {updated['retry']}")
print(f"Old reference unaffected: {999 not in updated['retry']['delays']}")
Solution
--- Basic isolation ---
Template after caller mutation: {'headers': {'Content-Type': 'application/json'}, 'retry': {'max': 3, 'delays': [1, 2, 4]}}
Caller copy has extra header: True
--- Input isolation ---
Template after raw mutation: [1, 2, 3]
--- Multiple callers ---
caller1 delays: [1, 2, 4, 8]
caller2 delays: [1, 2, 4]
template delays: [1, 2, 4]
All isolated: True
--- Update isolation ---
Template after update: {'max': 5, 'delays': [1, 2]}
Old reference unaffected: True

The copy-on-write contract:

The TemplateCache guarantees complete isolation through two rules:

  1. Copy on write (add/update): When a template is stored, deepcopy ensures the cache owns an independent copy. The caller can freely mutate their original reference without corrupting the cache.

  2. Copy on read (get): When a template is retrieved, deepcopy ensures the caller gets their own independent copy. The caller can freely mutate the returned value without corrupting the cache or affecting other callers.

Performance trade-off: Every add, update, and get call pays the cost of deepcopy. For hot paths with large templates, consider:

  • Caching the serialized form (JSON/pickle bytes) and deserializing on get — avoids the overhead of deepcopy's memo-dict traversal.
  • Using immutable data structures (@dataclass(frozen=True), tuple, frozenset) so no copy is needed at all.
  • Returning read-only views or proxies that raise on mutation.
Expected Output
--- Basic isolation ---
Template after caller mutation: {'headers': {'Content-Type': 'application/json'}, 'retry': {'max': 3, 'delays': [1, 2, 4]}}
Caller copy has extra header: True
--- Input isolation ---
Template after raw mutation: [1, 2, 3]
--- Multiple callers ---
caller1 delays: [1, 2, 4, 8]
caller2 delays: [1, 2, 4]
template delays: [1, 2, 4]
All isolated: True
--- Update isolation ---
Template after update: {'max': 5, 'delays': [1, 2]}
Old reference unaffected: True
Hints

Hint 1: Deep copy on both add (input) and get (output). This ensures: (1) the caller cannot mutate the stored template after adding it, and (2) the caller cannot mutate the stored template through a retrieved copy.

Hint 2: For `update_template`, deep copy the new value before storing. This ensures the caller's reference to the update dict cannot corrupt the cache.

© 2026 EngineersOfAI. All rights reserved.