Concepts / Debugging Unintended Side Effects

Debugging Unintended Side Effects

The important debugging question is not only “Which variable did I change?” Ask instead, “Which list object does this variable refer to?” With assignment, original and alias are two names for one object.

  • 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…

When One Change Appears Twice

A list can change in a place you did not expect. One common cause is assigning one list variable to another and assuming that a second list has been created. In Python, assignment creates a reference to the existing list, not a copy. If two variables refer to the same list, modifying the list through either variable changes what the other variable sees.

original[1, 2, 3]one list objectalias[1, 2, 3]new list objectcopyrefers torefers torefers to
Do these two variables contain separate lists, or do they point to the same list object?

Tracing Shared References

Code walkthrough
original = [1, 2, 3]
alias = original
alias[0] = 99
  1. 1
    After the first line, original refers to a list containing 1, 2, and 3.
  2. 2
    After the second line, alias refers to that same list. No separate list has been created.
  3. 3
    The third line changes the first element of the shared list to 99.
  4. 4
    Because both names refer to the shared list, original now also sees [99, 2, 3].
  • The list [1, 2, 3] is created.
  • Assignment makes alias another name for the same list.
  • Changing the first element through alias changes the shared list.
Output
original
[99, 2, 3]

alias
[99, 2, 3]
aliasalias[0] = 99[99, 2, 3]shared listoriginal[99, 2, 3]performschangesvisible through
How does a modification made through one variable reach and change the list observed through another variable?

The important debugging question is not only “Which variable did I change?” Ask instead, “Which list object does this variable refer to?” With assignment, original and alias are two names for one object. A mutation made through alias changes the object itself, so original observes the same change.

Making an Independent List

When the original list must remain unchanged, create a separate list before modifying it. The source identifies two ways to make a shallow copy: slice notation [:] and the list() constructor. Both create a new list with the same elements. A later modification to the copied list does not affect the original list.

python
Output
original
[10, 20, 30]

copy
[10, 200, 30]
Before
original = [10, 20, 30]
copy = original[:]

original and copy contain the same elements, but they are separate lists

After
copy[1] = 200

copy is [10, 200, 30]; original remains [10, 20, 30]

Replacing a Slice

Definition

Slice assignment uses the form t[start:end] = sequence. It replaces the elements selected by the slice with the elements in the replacement sequence. Because the replacement sequence can have a different number of elements, slice assignment can also change the list's length.

Code walkthrough
t = ['a', 'b', 'c', 'd', 'e', 'f']
t[1:3] = ['x', 'y']
  1. 1
    Before the assignment, t contains six elements.
  2. 2
    The slice t[1:3] refers to ['b', 'c'].
  3. 3
    The two selected elements are replaced by ['x', 'y'].
  4. 4
    The replacement has the same size as the selected slice, so t remains six elements long.
  • The slice t[1:3] selects the elements at positions 1 and 2.
  • Those elements are b and c.
  • The replacement sequence contains x and y.
0a1b1x2c2y3dreplaced byreplaced by
Which positions in the list are replaced when a slice such as t[1:3] receives a new sequence?

What do you think happens?

If a two-element slice is replaced by a three-element sequence, what happens to the list's length?

  • It stays the same

  • It decreases by one

  • It increases by one

Reveal answer
Answer:

It increases by one.

The replacement sequence has one more element than the slice being replaced, so the list grows by one element.

Predicting Length Changes

To predict the result of slice assignment, compare two counts: the number of elements selected by the slice and the number of elements in the replacement sequence. Equal sizes preserve the list's length. A shorter replacement makes the list shrink. A longer replacement makes the list grow.

Selected sliceReplacement sequenceLength effect
Two elementsTwo elementsLength stays the same
Two elementsOne elementList shrinks
Two elementsThree elementsList grows

Compare the selected slice size with the replacement sequence size.

Worked example

A copied list grows during slice assignment

Start with original = [10, 20, 30, 40, 50]. Create a separate copy, then replace the slice containing 30 and 40 with [200, 201, 202]. Predict both lists.

  1. 1
    Create the copy

    The copied list begins with the same five elements as original, but it is a separate list.

  2. 2
    Identify the selected slice

    The selected slice contains 30 and 40, so two elements will be replaced.

  3. 3
    Compare sizes

    The replacement sequence contains three elements, which is one more than the two selected elements.

  4. 4
    Apply the replacement

    The copied list grows from five elements to six elements. The original list is unchanged because the copy is separate.

Result

original remains [10, 20, 30, 40, 50], while the copied list becomes [10, 20, 200, 201, 202, 50].

[10, 20, 30, 40, 50]five elements[10, 20, 200, 201,202, 50]six elementsreplace two with three
What happens to the elements and length when a two-element slice is replaced by three elements?

Debugging Checklist

  • Common mistake

    Treating assignment as if it copied the list

    What it looks like
    alias = original
    Why it fails

    Both variables refer to the same list object, so a modification through alias changes what original sees.

    Do this instead

    Use original[:] or list(original) when an independent list is needed.

  • Common mistake

    Checking only the variable used for the mutation

    What it looks like
    alias[0] = 99
    Why it fails

    Another variable may refer to the same list and may therefore show the same change.

    Do this instead

    Trace every variable that refers to the list before deciding whether the mutation is unexpected.

  • Common mistake

    Assuming slice assignment preserves list length

    What it looks like
    t[1:3] = [200, 201, 202]
    Why it fails

    The replacement sequence is longer than the selected slice, so the list grows.

    Do this instead

    Count the selected elements and the replacement elements before applying the assignment.

  • Common mistake

    Copying when the program intentionally needs to update the original

    Why it fails

    A copy prevents the original list from changing, which may not match the program's intended logic.

    Do this instead

    Choose copying or in-place modification explicitly and document the reason for an intentional in-place change.

  1. 1
    Find every assignment involving the list.
  2. 2
    Mark which variables refer to the same list and which were made with a copy.
  3. 3
    Locate each mutation, including element assignment and slice assignment.
  4. 4
    For slice assignment, identify the selected elements and count the replacement elements.
  5. 5
    Decide whether the observed change is an intended in-place update or an unintended shared-reference mutation.

Try the Trace

MEDIUM

A program contains original = [1, 2, 3], alias = original, and alias[1:3] = [8]. Predict the value seen through original and decide whether the list grew, shrank, or stayed the same.

Hints
  • Assignment makes alias refer to the same list as original.
  • The slice [1:3] selects two elements.
  • The replacement sequence contains one element.
EASY

Rewrite the previous situation so that modifying the second list does not change original. Use one of the two copying techniques described in this article.

Hints
  • A slice with no start or end, such as original[:], creates a shallow copy.
  • The list() constructor can also create a separate list with the same elements.

Key Takeaways

  1. 1
    Assignment between list variables creates another reference to the same list; it does not create a copy.
  2. 2
    Use [:] or the list() constructor when you need a separate list with the same elements.
  3. 3
    Slice assignment replaces multiple selected elements and can change list length.
  4. 4
    A shorter replacement shrinks the list, an equal-sized replacement preserves its length, and a longer replacement grows it.
  5. 5
    When debugging a surprising mutation, trace shared references, the selected slice, and the size of the replacement sequence.

At a glance

  • Assignment creates a shared reference rather than an independent list.
  • Copy a list with [:] or list() before modifying it when the original must be preserved.
  • Slice assignment updates a range of elements and may grow or shrink the list.
  • Debug unintended side effects by tracing which variables share a list and counting slice elements.

Related concepts

More concepts starting with D