Concepts / Debugging Unexpected Changes in Data

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.

  • Programming
Interactive lab

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

  1. State the expected output from the specification, run the program and compare: a difference is the evidence you debug from.
  2. 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.
  3. An inserted print('debug: ...', value) shows the same state in the output; it must not change what the program itself prints.
  4. 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.
  5. 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.

Educational simulation

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?

  • Yes, because b is a copy

  • No, because a and b refer to the same list

  • Only if the list contains numbers

  • Only if a is printed before b

Reveal answer
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.

avariable name[1, 2, 3]one list objectbaliasrefers torefers to
What object do a and b refer to after executing b = a?
Key point

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

python

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]a[1, 2, 3, 4]b[1, 2, 3]b[1, 2, 3, 4]same list after mutationsame list after mutation
What changes in the shared list when b.append(4) is performed?
Output
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.

b[0] = 9mutationshared listone underlying objecta[0]9changesis read through
How does a mutation through one variable name affect the value observed through another name?

Lists and Strings

ObjectWhat assignment doesWhat an apparent change does
ListA second variable can refer to the same listA mutation changes the shared list, so aliases observe the change
StringA second variable can refer to the same stringAn operation that appears to change the string creates a new string instead

Aliasing has different practical consequences for mutable lists and immutable strings.

alist[1, 2, 3]can change in placeabananaBANANAnew stringbsame listbbananarefers torefers tostill refers to bananaafter b.upper()
What is the difference between changing a list through an alias and rebinding a string variable?
python

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.

Before
old_list = [1, 2, 3]
new_list = old_list
new_list.append(4)

new_list and old_list refer to the same list

After
old_list = [1, 2, 3]
new_list = old_list[:]
new_list.append(4)

new_list is an explicit copy, so the lists are independent

old_list[1, 2, 3]original listone list objectnew_list[1, 2, 3]copied listanother list objectrefers torefers to
Which variables share the same object, and where should a copy be made to prevent an unintended mutation?

Mistakes to Catch

  • Common mistake

    Assuming b = a creates a copy of a list.

    What it looks like
    a = [1, 2, 3]; b = a; b.append(4)
    Why it fails

    Both names refer to one underlying list, so the mutation is visible through a and b.

    Do this instead

    Use 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 like

    A list can be changed through an alias, but b = b.upper() creates a new string.

    Why it fails

    Lists are mutable, whereas strings are immutable according to the source material.

    Do this instead

    Ask 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 like

    a changes after b[0] is modified.

    Why it fails

    The result follows from a and b referring to the same list.

    Do this instead

    Trace the references and check whether the names are aliases.

EASY

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_list as its argument.

Debugging Checklist

  1. 1
    Find the variable whose value changed unexpectedly.
  2. 2
    Check whether another variable was assigned from it with a statement such as b = a.
  3. 3
    Determine whether the shared object is mutable, such as a list, or immutable, such as a string.
  4. 4
    For a list, look for a mutation performed through any alias.
  5. 5
    Use the is operator to check whether two names refer to the same object.
  6. 6
    Create 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.

Related concepts

More concepts starting with D