Diagnose repeated references at every depth and choose reconstruction, shallow copying, selective copying, or deep copying from explicit ownership needs.
You will learn: Trace nested aliases, recognize one-level copy operations, preserve intentional sharing, and choose selective or deep independence only where ownership requires it.
Practice in: Google Colab, JupyterLab, or a local editor
List repetition copied the reference to row three times. It did not evaluate a new row construction for every position. The cell assignment followed grid[0] to the one row object and mutated it. Every apparent row displays the same changed object.
This lesson answers:
At which depth do two structures still share mutable state?
What exactly does a shallow copy separate?
When is copying selected nested fields clearer than deepcopy?
Why can a deep-copied graph still contain intentional internal sharing?
2. Construct a fresh row for every position
A comprehension executes its expression once per iteration:
The outer list now holds references to three independently constructed rows. This is reconstruction, not a later copy operation. When you control creation, constructing the intended graph directly is often clearer than creating aliases and repairing them afterward.
3. Assignment, construction, and copying answer different questions
The explicit dictionary/list literals construct the objects written in the expression.
A copy operation follows a defined copy depth and may retain inner references.
Do not infer nested independence merely because the outer object is new. Follow every path that later code will mutate.
4. A shallow copy separates the outside and shares the inside
template = {"name": "scout","tasks": ["map gate"],"notes": {"entries": ["start"]},}shallow = template.copy()assert shallow isnot templateassert shallow["tasks"] is template["tasks"]assert shallow["notes"] is template["notes"]assert shallow["notes"]["entries"] is template["notes"]["entries"]
The new dictionary has its own set of key/value slots, but each value slot is initialized with a reference from the original. Replacing an outer slot remains local to one dictionary:
The copy is neither “broken” nor “half complete.” It correctly implements a one-level copy. The problem is choosing that depth for a contract that required nested independence.
Assignment shares the outer dictionary. A shallow copy creates another outer dictionary, but its task slot still points to the original nested list.
flowchart LR
A["name: template"] --> B["original dictionary"]
C["name: alias"] --> B
D["name: shallow"] --> E["new dictionary"]
B --> F["shared tasks list"]
E --> F
G["name: independent"] --> H["new dictionary"]
H --> I["new tasks list"]
5. Several familiar operations make shallow copies
For lists, these create distinct outer lists and reuse element references:
nested = [["map"], ["key"]]by_method = nested.copy()by_slice = nested[:]by_constructor =list(nested)assert by_method isnot nestedassert by_slice isnot nestedassert by_constructor isnot nestedassert by_method[0] is nested[0]assert by_slice[0] is nested[0]assert by_constructor[0] is nested[0]
For dictionaries, .copy(), dict(existing), unpacking, merge, and copy.copy() are one-level operations:
from copy import copysource = {"tasks": ["map"]}copies = [ source.copy(),dict(source), {**source}, source | {}, copy(source),]assertall(candidate isnot source for candidate in copies)assertall(candidate["tasks"] is source["tasks"] for candidate in copies)
These spellings have different uses and type constraints, but none recursively copies nested objects. Do not choose among them expecting a deeper ownership boundary.
6. Immutable nested values can remain shared safely
template = {"rules": ("keep the map", "share clues"),"difficulty": "foundation","tasks": ["find gate"],}shallow = template.copy()assert shallow["rules"] is template["rules"]assert shallow["difficulty"] is template["difficulty"]
The identity of immutable literals may be reused, but the design does not depend on it. More importantly, neither tuple’s slots nor string contents can be mutated. Sharing those particular values cannot leak an in-place content change.
The mutable tasks list is different. If each board may add tasks, that field must become independently owned. Copy decisions can therefore vary by field.
Selective copying makes the intended ownership visible. It requires maintenance when the data shape changes, which can be an advantage: adding a mutable field forces the designer to decide its ownership instead of silently copying everything.
deepcopy recursively copies supported mutable descendants and reuses immutable values when appropriate. It is concise when the complete mutable graph truly needs independent edits.
9. Deep copy preserves internal sharing in the new graph
Suppose two source paths intentionally refer to one list:
The copied graph has a new clues list separated from the source, but both clone paths still share that one new list. deepcopy remembers objects it has already copied so it can preserve relationships and handle recursive container graphs. It does not blindly duplicate one object every time another path encounters it.
That behavior is usually desirable: graph structure is part of the data. If the two clone fields must become independent despite being shared in the source, that is a transformation requiring explicit reconstruction, not ordinary deep copying.
10. Deep copy is not a universal safety button
Deep copying can:
consume significant time and memory for large graphs;
duplicate mutable objects that should remain intentionally shared;
preserve internal aliases that the new contract wanted to split;
interact with custom class copy hooks introduced later; and
be unsuitable or return unchanged references for resource-like or executable objects such as modules, functions, open files, sockets, and external sessions.
Do not use JSON encode/decode as a generic substitute. Serialization accepts a limited data model and can change tuples, sets, keys, bytes, custom objects, and other semantics. Unit 9 teaches external formats for their actual purpose.
11. Choose the strategy from the required future edits
Strategy
Choose it when
Evidence to record
intentional sharing
all owners should observe updates
relevant paths are identical
reconstruct
source data can clearly build the intended graph
each constructed mutable owner is distinct
shallow copy
only outer membership/fields change
outer identity differs; nested identities may match
selective copy
known descendants need separate ownership
specified mutable paths differ
deep copy
a supported complete graph needs independence
source and copied mutable descendants differ
Avoid “copy just in case.” State the future mutation and source-preservation promise first. The assertions then describe the minimum graph relationship that can satisfy it.
12. Lab: create independent mission boards
Implement a shallow comparison and a selectively owned board:
def shallow_board(template):"""Return a new outer dictionary that shares nested values."""raiseNotImplementedErrordef mission_board(template, *, owner):"""Return a board whose mutable task and note state belongs to owner."""raiseNotImplementedErrordef board_relationship(left, right):"""Return identity evidence for important board paths."""raiseNotImplementedError
Keep the immutable rules tuple available for safe sharing. The lab does not require that its identity be shared because equivalent construction or implementation choices can differ; its value and immutability satisfy the contract.
Hint: rebuild every mutable level named in the ownership table
Begin with a shallow outer copy. Replace tasks with a new dictionary whose values are new lists. Replace notes with a new dictionary and replace its entries with a new list. Rebind owner. The relationship helper uses is at each listed path.
Show one complete solution after attempting the lab
def shallow_board(template):"""Return a new outer dictionary that shares nested values."""return template.copy()def mission_board(template, *, owner):"""Return a board whose mutable task and note state belongs to owner.""" board = template.copy() board["owner"] = owner board["tasks"] = {}for direction, tasks in template["tasks"].items(): board["tasks"][direction] = tasks.copy() board["notes"] = template["notes"].copy() board["notes"]["entries"] = template["notes"]["entries"].copy()return boarddef board_relationship(left, right):"""Return identity evidence for important board paths."""return {"same_outer": left is right,"same_tasks": left["tasks"] is right["tasks"],"same_north_tasks": left["tasks"]["north"] is right["tasks"]["north"],"same_notes": left["notes"] is right["notes"],"same_note_entries": ( left["notes"]["entries"] is right["notes"]["entries"] ), }
Why would copying only the tasks dictionary still leak list edits?
What schema change would require updating mission_board?
What would deepcopy(template) make easier, and what ownership decisions would it make less visible?
Add an evidence list inside notes. Predict the leak before updating the copy function, observe it once, then give the new field independent ownership and rerun every source-preservation assertion.
Key points
TipKey points
Repetition can duplicate references rather than nested objects; construct a fresh mutable value for each intended owner.
A shallow copy creates a new outer container and reuses its contained references.
Immutable nested values can often remain shared safely.
Selective copying makes field-level ownership decisions explicit.
deepcopy separates a supported object graph from its source while preserving intentional sharing relationships inside the cloned graph.
Deep copy has time, memory, resource, and semantic tradeoffs; it is not a universal safety button.
Choose copy depth from the exact future mutations that must remain independent.