Shallow vs deep copy and shared references
A shallow copy creates a new outer container but fills it with references to the same inner objects. A deep copy recursively creates new copies of everything reachable, so the result shares no mutable state with the original. In the standard library, copy.copy gives the first and copy.deepcopy the second. list(x), x[:], x.copy(), dict(x) and set(x) are all shallow.
When shallow copies bite: any nested mutable structure (list of lists, dict of lists, objects holding collections). Mutating the 'copy' silently mutates the original, and the symptom often appears far from the cause, such as config dicts altered per request or the [[0]*n]*m grid bug. When deep copies bite: they are expensive (time and memory proportional to the whole object graph), they can copy things that were meant to be shared (a logger, cache, connection pool, singleton), they fail or misbehave on objects that cannot be copied (locks, sockets, file handles, some C extension objects), and they can break identity assumptions elsewhere in your program.
How deepcopy works and how to customize it: it keeps a memo dict mapping id(original) to the new object, which is how it handles cycles and preserves shared references inside the copied graph (two attributes pointing to one object still point to one object in the copy). You can control behaviour with copy and deepcopy (for example, copy the data but keep a reference to the shared connection).
Alternatives I reach for in real systems: avoid the problem with immutable data (tuples, frozen dataclasses, dataclasses.replace), copy only what you will mutate (copy-on-write at the path you change), or use persistent data structures. Common mistake in interviews: saying 'copy() copies the object' without stating what is shared. Always say what is shared, then give the bug it causes.
0-2 years experience
2-5 years experience
5-8 years experience
8+ years experience