List Methods and Mutations
Lists are passed to functions by reference, meaning the function receives access to the original list, not a copy.
The Surprise After a Function Call
A function can change a list that was created outside the function. This may seem surprising if you expect the function to receive its own copy. In Python, passing a list gives the function access to the original list object. Therefore, a change made to that object inside the function is visible to the caller after the function returns.
What do you think happens?
Before tracing the function call, predict the contents of letters after delete_head receives it and deletes the element at index 0.
Reveal answer
Answer: ['b', 'c']
The function parameter refers to the same list object as letters. Deleting the element at index 0 changes that shared object, so the caller sees the deletion.
One List and Two Names
Aliasing means that two names refer to the same list object. When a list is passed to a function, the original variable and the function parameter become aliases for that one object in memory. They are different names, but they provide access to the same list.
The list is not duplicated when the function receives it. Instead, the function receives a reference to the existing list object. The parameter t and the caller's variable letters therefore access the same object. If t changes that object, letters shows the change because it still refers to that object.
Tracing a Deletion
The delete_head example
A list named letters contains ['a', 'b', 'c']. The function delete_head receives that list as parameter t and deletes the element at index 0. What does letters contain after the function call?
Before the call: The caller's variable letters refers to the list ['a', 'b', 'c'].
During the call: The parameter t refers to the same list object as letters. The deletion at index 0 removes 'a' from that object.
After the call: The list object now contains ['b', 'c']. The name letters still refers to that object, so the caller sees the shorter list.
letters contains ['b', 'c'] after delete_head returns.
The important event is not that the parameter disappears when the function ends. The important event is that the shared list object was changed while the function was running. Because letters refers to that same object, the deletion remains visible after the function returns.
Mutation Versus Reassignment
| Action inside the function | Effect on the caller's list |
|---|---|
| Modify the list object itself | The caller sees the modification |
| Reassign the parameter to another list | The original list is not affected by that reassignment |
Reassignment changes the parameter's reference, not the original list object. For example, if a function reassigns its parameter to [1, 2, 3], that parameter now refers to another list, but the caller's original list remains unaffected by the reassignment. In contrast, deleting elements from the list through the parameter changes the shared object and is visible to the caller.
Finding the Unexpected Change
Unexpected list mutations are usually found by tracing every function call that received the list. Any such function could have modified the shared object. Record the list's contents before and after each call, then identify the first call after which the contents differ.
Assuming the function receives a separate copy of the list.
The parameter and the caller's variable are aliases for the same list object.
Fix:
Before calling the function, predict whether the function will modify the list object itself.Treating reassignment and mutation as the same operation.
Reassignment changes what the parameter refers to; it does not change the caller's original list.
Fix:
Determine whether the function changes the shared list or only changes the parameter's reference.Checking only the final state when debugging.
The final state does not identify which function changed the list.
Fix:
Inspect the list before and after each function call that receives it.
Practice the Trace
A caller has a list containing ['x', 'y', 'z'] and passes it to a function. Inside the function, the parameter is used to delete the element at index 0. Predict the list contents seen by the caller after the function returns. Then explain whether the result is caused by mutation or reassignment.
Hints
- The parameter refers to the same list object as the caller's variable.
- Deleting the element at index 0 changes the list object itself.
- Compare the list before the call with the list after the deletion.
A list named data is passed to a function called remove_negatives. The function deletes elements using del numbers[i]. Before the call, data contains several values. What should you inspect if data has changed unexpectedly after the function returns?
Hints
- The function receives a reference to data.
- Deleting elements through numbers modifies the actual list object.
- Trace the contents of data before and after the function call.
Key Takeaways
- Passing a list to a function gives the function a reference to the original list rather than a copy.
- The function parameter and the caller's variable are aliases for the same list object.
- A modification to the shared list is visible to the caller after the function returns.
- Reassigning the parameter does not change the caller's original list.
- To debug an unexpected mutation, trace every function call that received the list and compare the list before and after each call.
Key Takeaways
- A list argument gives a function access to the original list object.
- The function parameter and the original list variable are aliases.
- Deleting or otherwise modifying the shared list changes what the caller sees.
- Reassigning the parameter is different from modifying the shared list.
- Debug list mutations by tracing the state before and after each function call.