Skip to content
intermediate Phase 15 · Python Standard Library Deep Dive

copy & Data Immutability

Use copy/deepcopy and understand mutable vs immutable data semantics.

30m
0 problems
Topic Progress 0%

Aliasing, Shallow Copies, and Immutability

Aliasing, Shallow Copies, and Immutability

Two names for one object versus two objects that share inner pieces:

a = [1, [2, 3]]
b = a                 # alias: same object (a is b → True)
c = a.copy()          # shallow: new outer list, same inner list
d = [[*a[0]]]         # wrong shape — build copies explicitly instead

c.append(4); print(a)             # a unchanged
c[1].append(99);  print(a[1])     # [2, 3, 99] — inner object was shared

Immutable values (int, str, tuple-of-immutables, frozenset) can be aliased freely: nothing can change them. Mutable containers need real copies when independence matters. Slicing a[:], list(a), and a.copy() are all shallow — they duplicate only the outermost layer.

copy.copy and copy.deepcopy

copy.copy and copy.deepcopy

import copy

src = {"users": [{"name": "ann"}], "meta": object()}
shallow = copy.copy(src)     # outer dict new; "users" list still shared
deep    = copy.deepcopy(src) # every level rebuilt; fully independent

deepcopy maintains a memo dict of already-visited ids, so cyclic structures (x.self = x) terminate instead of recursing forever. It deliberately does not copy functions, classes, or module objects — it returns the same ones, since they are effectively immutable code.

For dataclasses, copy.deepcopy works, but the idiomatic update is replace(item, price=9): it constructs a fresh instance with one field swapped, and it works on frozen=True objects where direct assignment raises FrozenInstanceError.

Pitfalls and Interview Angles

Pitfalls and Interview Angles

Mutable default arguments — def add(item, bucket=[]) — create one list per function definition, shared across every call forever. Use bucket=None then bucket = bucket or [].

Frozen is shallow. A frozen dataclass can still hold a mutable list; freezing blocks rebinding, not mutation of the field’s object.

is vs ==: is tests identity (cache-dependent for small ints and short strings — CPython caches roughly -5..256), == tests value. Never rely on is for numeric equality.

Interview angle: answer "shallow vs deep copy" with the shared-inner-list demo above, then explain why deepcopy is expensive (full allocation per node) and when a hand-written clone beats it in hot paths.

Quiz

1. After c = a.copy(); c[1].append(99), why does a[1] change?

Question 1 options

2. How do you correctly change one field of a frozen dataclass?

Question 2 options

3. What does deepcopy do with a self-referencing structure?

Question 3 options

Flashcards

Question

Shallow copy

Answer

New outer container, inner objects shared — copy.copy, list(a), a.copy().

Question

Deep copy

Answer

Rebuilds every level (memo handles cycles) — copy.deepcopy; fully independent result.

Question

Mutable default argument

Answer

def f(x, acc=[]) shares one list for the function’s lifetime; default to None instead.

Question

dataclasses.replace

Answer

Creates a copy with fields overridden — the way to “update” frozen dataclasses.

Revision Notes

Key Takeaways

  • 1. Aliasing shares everything; shallow copies share inner objects; deep copies share nothing
  • 2. Use replace() for dataclass updates and default_factory/None for safe defaults
  • 3. is checks identity, == checks value — small-int caching makes the distinction sharper

Interview Tips

  • • Demonstrate shallow vs deep with a 4-line snippet showing the inner list leaking
  • • Connect frozen dataclasses to immutability benefits and name replace() as the update path

Cheat Sheet

alias = same object; copy()/copy.copy shallow; deepcopy deep (memo→cycles); mutable default → None; frozen update via replace(); is=identity, ==value.

Continue Learning