Why does `dict.copy()` leave nested lists shared with the original dict?
answer
- It copies exactly one level
- New outer container, same inner objects
- Rebinding a key differs from mutating a value
- Multiplying a list repeats one object
- Deep copy walks the whole graph
basics
~20 sdict.copy() is shallow: it builds a new dict whose values are the very same objects as the original's. Replacing a key in the copy is independent, but mutating a nested list is seen by both dicts. copy.deepcopy duplicates the nested objects too.
solid answer
~50 sA shallow copy duplicates exactly one level: `d.copy()`, `dict(d)`, `list(a)`, `a[:]` and `copy.copy(obj)` all build a new outer container whose slots are bound to the **same inner objects**. So `new = d.copy(); new['k'] = 1` is independent — that rebinds a key in the new dict — while `new['rules'].append('x')` mutates an object both dicts point at. The same one-level rule explains `[[0] * 3] * 3`, where the outer list holds one inner list three times, and `copy.copy` on your own class, which makes a new instance whose `__dict__` values are shared. The fix depends on the shape: `copy.deepcopy(d)` for an arbitrary graph, an explicit rebuild such as `{k: v[:] for k, v in d.items()}` when you know the shape and want the cheaper option, or immutable values (tuples, frozensets) so sharing stops mattering at all.
code
python · 14 linesimport copy
original = {"rules": ["velocity", "geo"], "threshold": 83}
shallow = original.copy()
shallow["threshold"] = 90
shallow["rules"].append("device")
print(original["threshold"]) # 83 - independent
print(original["rules"]) # shared list, now has 'device'
deep = copy.deepcopy(original)
deep["rules"].append("ip")
print(original["rules"]) # unchanged by the deep copygo deeper
Know that a shallow copy duplicates only the outer container, and be able to show the two-line demo where appending to a nested list in the copy also changes the original.
Explain the rebind-versus-mutate distinction precisely, name the shallow idioms (dict.copy, slicing, list(), copy.copy) and give a fix that is not just a blanket deep copy.
Judge which level actually needs duplicating, spot a defensive copy that is one level too shallow in review, and weigh a rebuild against copy.deepcopy on cost and on the shape you control.
Push designs toward immutable values or constructed derived state so the shallow-versus-deep question disappears, rather than sprinkling defensive copies through the layers that read state.
### What "shallow" actually means A shallow copy creates **one** new object — the outer container — and fills it with references to the exact objects the original held. It copies the structure at the top level and shares everything below it. Every one-level copying idiom in Python behaves this way: * `d.copy()` and `dict(d)` for a dict * `a.copy()`, `a[:]` and `list(a)` for a list * `s.copy()` and `set(s)` for a set * `copy.copy(obj)` for anything, including instances of your own classes Because the inner objects are shared, two different-looking operations on the copy have opposite consequences: ```python import copy original = {"rules": ["velocity", "geo"], "threshold": 83} shallow = original.copy() shallow["threshold"] = 90 # rebinds a key in the new dict only shallow["rules"].append("device") # mutates the list BOTH dicts point at print(original["threshold"]) # 83 print(original["rules"]) # ['velocity', 'geo', 'device'] ``` Rebinding a key touches only the new dict's own slot. Mutating a value reaches through the shared reference. That single distinction — rebind versus mutate — is the whole of shallow-copy behaviour. ### The same rule in disguise The famous grid trap is the identical mechanism written with multiplication: ```python grid = [[0] * 3] * 3 grid[0][0] = 1 print(grid) # [[1, 0, 0], [1, 0, 0], [1, 0, 0]] ``` `[x] * 3` builds a list holding the *same* object three times; it never calls anything to duplicate `x`. The inner `[0] * 3` runs once, and all three rows are that one list. `[[0] * 3 for _ in range(3)]` evaluates the inner expression per iteration and gives three distinct rows. Slicing a list of lists (`rows[:]`) has exactly the same limitation: new outer list, same row objects. For your own classes, `copy.copy(obj)` (with no `__copy__` hook defined) produces a new instance of the same class whose `__dict__` is a shallow copy of the original's — notably **without calling `__init__`**. Any attribute holding a list or a dict is then shared between the two instances, which is the class-shaped version of the same bug. ### When shallow is the right answer Shallow copying is not a defective deep copy; it is the correct and cheap choice whenever the contained values are immutable or intentionally shared: * A dict of strings and numbers — `int`, `str`, `float`, `tuple` of those — cannot be mutated, so sharing them is unobservable and a shallow copy is genuinely independent. * Large read-only reference data that every copy should keep pointing at. Deep-copying it would duplicate megabytes for no behavioural gain. * Snapshotting a set of keys or a working list of items you will only rebind, never mutate in place. The cost difference is real: a shallow copy is proportional to the container's length, while a deep copy is proportional to the size of the entire reachable object graph and allocates a new object for every node in it. ### Making a copy that is actually independent Four options, in rough order of how often they are the right one: 1. **Rebuild with the shape you know.** `{k: list(v) for k, v in d.items()}` or `[row[:] for row in grid]` is explicit, fast and obvious to a reader. Use it when the nesting is one or two known levels deep. 2. **`copy.deepcopy(d)`.** Correct for an arbitrary graph, including shared and cyclic references, but it walks everything and can duplicate far more than you intended. 3. **Make the values immutable.** Store tuples instead of lists, `frozenset` instead of `set`, or frozen dataclass instances. Then a shallow copy is enough forever, and so is no copy at all. 4. **Do not copy — construct.** Often the honest fix is to build the derived structure from its inputs rather than to duplicate and patch an existing one. ### Diagnosing it in a review Two smells are worth reacting to immediately. The first is a defensive copy that is one level too shallow: `self._config = config.copy()` where `config` holds nested lists, which reads as protective but is not. The second is a copy used as a "snapshot" of state that will be mutated element-wise later — a rollback that restores the outer container but leaves the mutated inner objects in place. Both look correct in a diff and only fail when the nested value is actually mutated. A useful mental check while writing the copy: *for each value in this container, would I be upset if someone mutated it through the other reference?* If the answer is no for every value — because they are immutable or deliberately shared — a shallow copy is right. If it is yes for even one, either deep-copy that value specifically or change the design so the value cannot be mutated.
- How would you copy a dict of lists so nothing is shared, without reaching for copy.deepcopy?Rebuild it with the nesting you know: `{k: list(v) for k, v in d.items()}` gives a new dict and a new list per key. It is faster than a deep copy, it does not touch anything deeper than you intended, and a reader can see exactly which level is duplicated. Reserve `copy.deepcopy` for graphs whose shape you do not control.
- What does copy.copy do for an instance of a class you wrote yourself?By default it creates a new instance of the same class without running `__init__`, then gives it a shallow copy of the original's `__dict__`. Attributes holding immutable values are effectively independent; any attribute holding a list, dict or set is shared with the original. A class that needs different behaviour defines `__copy__`.
- Why is a shallow copy sometimes the deliberately correct choice?When the values are immutable, sharing is unobservable and copying them would be wasted work. When the values are large read-only reference data, sharing is the point — a deep copy would duplicate the data per snapshot for no behavioural difference. Shallow copying costs time proportional to the container's length; deep copying costs time and memory proportional to the whole reachable graph.
saying these in an interview costs you the question
- Thinks dict.copy() recursively duplicates nested values
- Believes slicing a list of lists gives independent rows
- Reaches for copy.deepcopy by default everywhere
- Says list(d) copies a dict rather than its keys
- Thinks assigning a key in the copy mutates the original
- Expects [[0] * 3] * 3 to build three separate rows