Debugging Unexpected Changes in Data
In an aliasing bug, the unexpected path is short: one name performs a mutation, the shared list changes, and another name later reads the changed list.
Try it: Debug by Tracing Execution
How to debug a program that runs but gives the wrong answer: compare expected and actual output, trace the lines in the order they really run with a variable watch table (or an inserted debug print), find the first line where a value goes wrong, then choose a fix and re-run to check it.
How it works
- State the expected output from the specification, run the program and compare: a difference is the evidence you debug from.
- Trace in execution order, not file order: loops repeat lines, if/elif picks one branch, and the watch table records every variable before each line runs.
- An inserted print('debug: ...', value) shows the same state in the output; it must not change what the program itself prints.
- Look for the first place a value differs from what it should be (an off-by-one range, > instead of >=, a total reset inside the loop, two names for one list, a str from input()). Where the wrong value shows up is only the symptom.
- Change one thing with a reason, re-run, and compare again: a fix that makes an error disappear can still leave the output wrong.
Default run (14 steps): Sum 1..n: Add up the whole numbers from 1 to n (n included) and print the sum. Press Play to trace it line by line. … Finished, but the output is wrong: got sum of 1 to 5 is 10, expected sum of 1 to 5 is 15.
Simplified: The lab's own small Python interpreter runs fixed, hand-written programs (you pick the program, the fix and a value; your text is never executed). Every program the lab can show was run in CPython 3.12 and the lab reproduces its output, its traceback text and its line-by-line trace. Six short programs, one editable value each; input() receives the value you type in the lab. Lines starting with "debug:" are ignored when comparing the output with the expected output.
Loading the simulation…
The Unexpected Change
A common debugging surprise begins with a belief that assigning b = a creates a separate copy of whatever a refers to. For mutable objects such as lists, that belief is incorrect. The assignment creates another variable name associated with the same object. If the object is then changed through one name, the change can be observed through the other name.
What do you think happens?
Suppose a list is assigned to a, then b = a is executed. If b is used to change the list, will a still show the original list?
Reveal answer
No, because a and b refer to the same list.
The assignment b = a makes b an alias for a. Both names refer to one underlying list, so a mutation through b is visible through a.
Two Names, One List
A reference is the association between a variable name and an object. When b = a is assigned, Python associates both names with the same object. The second name is called an alias for the first name in this situation. The important point is that the assignment does not create a second list.
The is operator checks whether two variables refer to the same object. If a is b evaluates to True, the two names are aliases for that object.
Tracing a List Mutation
After the first assignment, a refers to a list containing 1, 2, and 3. The statement b = a gives the same list a second name. The append operation changes that one list in place. Consequently, both a and b now provide access to the list containing 1, 2, 3, and 4.
a: [1, 2, 3, 4]
b: [1, 2, 3, 4]Following the Bug
When an unexpected value appears, trace the data from the operation that changed it to every variable that can observe it. In an aliasing bug, the unexpected path is short: one name performs a mutation, the shared list changes, and another name later reads the changed list. There is no second list whose contents could remain independent.
Lists and Strings
| Object | What assignment does | What an apparent change does |
|---|---|---|
| List | A second variable can refer to the same list | A mutation changes the shared list, so aliases observe the change |
| String | A second variable can refer to the same string | An operation that appears to change the string creates a new string instead |
Aliasing has different practical consequences for mutable lists and immutable strings.
After b = a, both names can refer to the string banana. The expression b.upper() does not modify that string in place. It creates the string BANANA, and b is rebound to the new string. The variable a continues to refer to banana. This is why aliasing is not a practical problem for strings in the same way it is for lists.
Preventing Shared-List Bugs
If two lists must be independent, create an explicit copy instead of assigning the original list to a second name. The source material gives two list-copying strategies: slicing with new_list = old_list[:] and using the list constructor with new_list = list(old_list). After either strategy, changes to the new list are intended to be separate from changes to the original list.
old_list = [1, 2, 3]
new_list = old_list
new_list.append(4)new_list and old_list refer to the same list
old_list = [1, 2, 3]
new_list = old_list[:]
new_list.append(4)new_list is an explicit copy, so the lists are independent
Mistakes to Catch
Common mistake Assuming b = a creates a copy of a list.
What it looks likea = [1, 2, 3]; b = a; b.append(4)Why it failsBoth names refer to one underlying list, so the mutation is visible through a and b.
Do this insteadUse a slice or the list constructor when an independent list is required.
Common mistake Treating a list mutation as if it were string reassignment.
What it looks likeA list can be changed through an alias, but b = b.upper() creates a new string.
Why it failsLists are mutable, whereas strings are immutable according to the source material.
Do this insteadAsk whether the operation changes the existing object or creates a new object and rebinds a variable.
Common mistake Calling shared-list behavior a Python error.
What it looks likea changes after b[0] is modified.
Why it failsThe result follows from a and b referring to the same list.
Do this insteadTrace the references and check whether the names are aliases.
A program needs two independent lists. One variable already refers to a list named old_list. Write the assignment that creates new_list as an explicit copy using slicing, and then write the equivalent assignment using the list constructor.
Hints
- The slicing form uses the complete slice of
old_list. - The constructor form receives
old_listas its argument.
Debugging Checklist
- 1Find the variable whose value changed unexpectedly.
- 2Check whether another variable was assigned from it with a statement such as b = a.
- 3Determine whether the shared object is mutable, such as a list, or immutable, such as a string.
- 4For a list, look for a mutation performed through any alias.
- 5Use the is operator to check whether two names refer to the same object.
- 6Create an explicit list copy with slicing or the list constructor when independent data is required.
Key Takeaways
- A reference associates a variable name with an object.
- The assignment b = a makes b an alias for a; it does not create a second list.
- Mutating a shared list through one alias changes the one underlying object seen through every alias.
- Strings cannot be changed in place, so an apparent string modification creates a new string and rebinds the variable.
- Use slicing or the list constructor to create independent lists when shared mutable data would cause a bug.