Concepts / Debugging Programs with Print Statements

Debugging Programs with Print Statements

The important debugging insight is that the error is not caused by the split operation itself.

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

Two Ways to Keep Input

Debugging becomes easier when you can see how a program's data changes. Consider a program that reads numbers and computes an average. One approach keeps only a running total and a count. Another stores every number in a list. Both can produce the same average, but they preserve different information. The running-total approach is compact and uses less memory. The list approach keeps the original data, which is valuable when you need further analysis or want to inspect what the program actually received.

Key point

The most useful question is not only “What result did the program produce?” but also “What data did the program retain while producing it?”

Tracing Both Methods

10total 10, count 110[10]20total 30, count 220[10, 20]30total 60, count 330[10, 20, 30]add 20add 30append 20append 30
How do the total, count, list contents, and average change as each input value is processed?

The two approaches agree on the final average, but their intermediate state is different. After processing 10, 20, and 30, the running-total method retains total equal to 60 and count equal to 3. The list method retains [10, 20, 30]. The list gives you more evidence about what happened during the loop: you can inspect each original value instead of seeing only the combined result.

Running totaltotal 60, count 3Individual valuesavailableIndividual valuesnot retainedNumber list[10, 20, 30]preservesdoes not preserve
What data remains available after the loop in each approach?

Averages from Collected Data

[10, 20, 30]collected valuessum()60len()3Average20.0totalcountdivide by countdivisor
How do collected values flow through sum() and len() to produce an average?
Definition

sum() takes a list of numbers and returns their total. len() takes a sequence such as a list and returns the number of elements. Together, sum(numbers) and len(numbers) provide the two quantities needed for an average.

Worked example

Inspecting a List Before Computing

A program has collected the values 8, 12, and 16. Determine the average and identify what each operation contributes.

  1. 1
    Collected list

    The list is [8, 12, 16], so the original input values are still available.

  2. 2
    Total

    sum([8, 12, 16]) produces 36.

  3. 3
    Count

    len([8, 12, 16]) produces 3.

  4. 4
    Average

    The program divides 36 by 3.

Result

The average is 12.0, and the list can still be inspected for later analysis.

Finding the Failing State

A traceback identifies where a program stopped, but it may not show which input caused the failure. A print statement placed immediately before the failing line can reveal the variable's exact state at that moment. A prefix such as “Debug:” separates diagnostic output from the program's regular output.

Code walkthrough
for line in lines:
    words = line.split()
    print("Debug:", words)
    if words[0] == "From":
        print(words[2])
  1. 1
    The debug output is produced immediately before the operation that can fail.
  2. 2
    If the last debug line shows an empty list, the program's input state is visible before the traceback.
  3. 3
    The failing operation is words[0], which requires an element at index 0.
  • The line is split into a list of words.
  • The debug statement displays that list before index 0 is accessed.
  • The next condition assumes that words contains at least one element.
Input lineblank linewords[]Debug outputDebug: []words[0]IndexErrorsplitprint statethen access index 0
Which variable state is visible immediately before the program attempts the operation that crashes?
Key point

Place the diagnostic print as close as possible to the failing operation. The closer it is, the more directly its output describes the state that caused the failure.

Blank Lines and Empty Lists

When a file contains a blank line, splitting that line produces an empty list. An empty list has no element at index 0. Therefore, code that evaluates words[0] fails with IndexError: list index out of range. The important debugging insight is that the error is not caused by the split operation itself. The failure occurs because the program assumes that the result of split() always contains at least one word.

Output
Debug: ['From', 'person@example.com', 'Monday']
Monday
Debug: []
IndexError: list index out of range
Blank lineno wordsEmpty list[]Index 0missingIndexErrorlist index out of rangesplitaccess words[0]no element exists
What happens to a blank input line after splitting, and how does that empty list cause the failure?

Guarding Before Access

The guardian pattern checks that data is valid before the program uses it. For a list produced by splitting a line, the guard can check whether len(words) is greater than zero. Only non-empty lists continue to the code that accesses words[0]. Empty lists are skipped, so the protected access cannot produce an IndexError from that condition.

Code walkthrough
for line in lines:
    words = line.split()
    print("Debug:", words)

    if len(words) > 0:
        if words[0] == "From":
            print(words[2])
  1. 1
    The program splits the current line into words.
  2. 2
    The debug statement shows the parsed list.
  3. 3
    The guard checks that the list contains at least one element.
  4. 4
    Only then does the program access words[0].
  5. 5
    A blank line produces [], fails the guard, and is skipped.
Split linewordsparsed listlen(words) > 0words[0]Skip lineproducesvalidateyesno
How does control flow change when the program checks whether parsed data exists before accessing an index?
Best practice

Use print debugging to discover the unexpected state, then use a guard to handle that state. The print explains what happened; the guardian changes the control flow so the same input does not crash the program again.

Common Debugging Mistakes

  • Common mistake

    Keeping only a running total when later analysis is required

    What it looks like

    The program retains total and count but later needs to identify the largest original value.

    Why it fails

    The individual numbers were not stored, so the original data is no longer available through that method.

    Do this instead

    Store the values in a list when you need multiple statistics, comparisons, or direct inspection of the input.

  • Common mistake

    Assuming every parsed line contains a word at index 0

    What it looks like

    The program evaluates words[0] immediately after line.split().

    Why it fails

    A blank line produces an empty list, which has no index 0.

    Do this instead

    Check that the parsed list is non-empty before accessing an index.

  • Common mistake

    Printing a variable too far away from the failure

    What it looks like

    The program prints general information at the start of a loop but not the parsed value before words[0].

    Why it fails

    The displayed state may no longer describe the input that caused the crash.

    Do this instead

    Add a Debug: print immediately before the failing operation.

  • Common mistake

    Treating the traceback as a complete explanation

    What it looks like

    The programmer sees IndexError and changes the index without inspecting words.

    Why it fails

    The traceback identifies the failure location, but the unexpected input explains why the assumption failed.

    Do this instead

    Inspect the relevant variable, identify the malformed or empty input, and then add an appropriate guard.

Practice the Trace

EASY

A loop processes the values 4, 9, and 11. Trace both approaches after each value. For the running-total approach, record total and count. For the list approach, record the list contents. Then calculate the final average using sum() and len().

Hints
  • Start the running total at 0 and the count at 0.
  • Start the list as [].
  • After processing each value, update the state before moving to the next value.
  • The final list should contain all three original values.
MEDIUM

A file parser prints “Debug: []” immediately before it crashes on words[0]. Explain the chain of events from the input line to the IndexError, then write the guard that should appear before the indexed access.

Hints
  • An empty debug list means the current line produced no words.
  • Index 0 requires at least one list element.
  • Use a condition based on len(words) before accessing words[0].

What do you think happens?

What will the debug statement display for a blank line after words = line.split()?

  • Debug: ['']

  • Debug: []

  • Debug: None

  • Debug: ['blank']

Reveal answer
Answer:

Debug: []

A blank line contains zero words, so splitting it produces an empty list. Accessing words[0] afterward causes IndexError because the list has no first element.

Debugging Checklist

  1. 1
    Decide whether you need only a final result or need to preserve the original values.
  2. 2
    Use total and count for a simple running average when the data will not be needed later.
  3. 3
    Use a list when you need further analysis, comparisons, or a clear view of the collected input.
  4. 4
    Use sum() for the list total and len() for the number of list elements.
  5. 5
    When a failure occurs, print the relevant variable immediately before the failing line.
  6. 6
    If the debug output shows an empty or malformed value, compare the program's assumption with the actual input.
  7. 7
    Add a guardian condition so invalid or empty data is not used as though it were valid.
Key point

Print debugging reveals the state that led to a failure. The guardian pattern prevents the program from using a state it cannot safely handle. Together, they turn an unexplained crash into an observable input problem with a defensible control-flow solution.

Key Takeaways

  • Running totals retain only a sum and a count, while list storage retains every original value.
  • sum() and len() provide the total and element count needed to calculate an average from a list.
  • A print statement immediately before a failing operation can reveal the exact variable state that caused the failure.
  • A blank line can split into an empty list, and accessing index 0 of that list raises IndexError: list index out of range.
  • The guardian pattern checks that parsed data is valid before indexed access, preventing crashes caused by empty or malformed input.

Related concepts

More concepts starting with D