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?
2. How do you correctly change one field of a frozen dataclass?
3. What does deepcopy do with a self-referencing structure?
Flashcards
Question
Shallow copy
Click to reveal answer
Answer
New outer container, inner objects shared — copy.copy, list(a), a.copy().
Question
Deep copy
Click to reveal answer
Answer
Rebuilds every level (memo handles cycles) — copy.deepcopy; fully independent result.
Question
Mutable default argument
Click to reveal answer
Answer
def f(x, acc=[]) shares one list for the function’s lifetime; default to None instead.
Question
dataclasses.replace
Click to reveal answer
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.