FREE LESSON · Programming & program design · 3 OF 8
Mutation, aliasing, and ownership of data
Mutation, aliasing, and ownership of data: Predict when a change is visible through more than one name.
Prerequisites: Values, assignment, functions, and lists.
Learning objectives
- Distinguish rebinding from mutation.
- Trace shared references and shallow copies.
- Specify whether a function may mutate its inputs.
Names can reach the same object
Assignment does not necessarily copy an object. Two names can refer to the same mutable list. Mutating that list changes what both names observe; rebinding one name to a different list does not redirect the other name. Draw boxes for objects and arrows for references when a textual trace becomes difficult.
first = [2, 4]
second = first
second.append(6)
assert first == [2, 4, 6]
second = [9]
assert first == [2, 4, 6]
assert second == [9]
The append changes the shared object. The later assignment changes only second's reference. This distinction explains why a function receiving a list may alter the caller's data without returning a replacement. Its parameter initially refers to the same object as the argument. Reassigning the parameter alone does not rebind the caller's variable.
Copies have depth
A shallow copy creates a new outer collection while retaining references to its elements. If those elements are mutable, inner changes remain shared. Consider rows=[[1],[2]] and snapshot=list(rows). Appending a new outer row to rows does not change snapshot, but appending to rows[0] changes the first inner list observed by both. A deep copy attempts to copy a whole reachable structure, but its cost and treatment of shared identity require thought.
Repeated references are another source of surprises. Constructing three rows by repeating one inner list gives three references to the same row. Updating one cell can appear to update every row. Construct each row independently when independent state is intended. An object graph diagram makes the difference between three objects and three arrows to one object explicit.
Make mutation a contract decision
A sorting function can either modify its input or return a new sorted collection. Both designs can be useful, but callers must know which promise holds. Hidden mutation makes local reasoning difficult because a seemingly unrelated reference can observe the change. Returning internal mutable storage from an object can similarly allow callers to break its invariant.
Immutability removes many of these risks but does not make all operations free. Persistent data structures share unchanged structure safely; copying an entire large collection on every update can be expensive. Ownership conventions, immutable values, defensive copying, and narrow mutation interfaces are alternative tools. Choose based on the required semantics and resource constraints. Tests should inspect both the result and the state that the contract says must remain unchanged, including nested data when relevant.
Practice problem
A function receives a list of student dictionaries and returns the students whose score meets a cutoff. Its author copies the outer list and then adds a passed flag to each selected dictionary. Explain whether the original records change. Give two possible valid contracts and describe how each should be implemented. Construct a test that detects unwanted mutation of a nested field, and explain why checking only that the returned outer list has a different identity is insufficient.
Worked solution
The copied list still contains references to the original dictionaries, so assigning passed changes records observed by the caller. One valid contract explicitly allows updating original records and returning selected references; tests should confirm exactly which records change. Another promises to leave inputs untouched and return independent records. For flat dictionaries, copying each dictionary before adding the flag may satisfy that promise. If records contain mutable nested lists or dictionaries, a shallow dictionary copy still shares those objects, so independence needs a deeper copying policy or immutable output representation. A test should retain an expected nested value, invoke the function, modify a returned nested field, and verify the original remains unchanged when independence is promised. Different outer identities alone say nothing about shared descendants.
Check your understanding
After b = list(a), where a contains mutable inner lists, which change can still be visible through both a and b?
- Appending a new outer element to b.
- Rebinding b to an empty list.
- Appending an element to a shared inner list.
Explanation
The outer list was copied, but its elements remain references to the same inner objects.
What does assigning a new list to a function parameter do by itself?
- It rebinds the local parameter.
- It rebinds every caller variable referring to the old list.
- It mutates the old list.
Explanation
Assignment changes that local name; mutation requires an operation on the referenced object.
Primary reading
- Structure and Interpretation of Computer Programs: University of California, Berkeley
- The Python Tutorial: Python Software Foundation