Debugging Programs with Print Statements
The important debugging insight is that the error is not caused by the split operation itself.
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…
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.
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
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.
Averages from Collected Data
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.
Inspecting a List Before Computing
A program has collected the values 8, 12, and 16. Determine the average and identify what each operation contributes.
- 1Collected list
The list is [8, 12, 16], so the original input values are still available.
- 2Total
sum([8, 12, 16])produces 36. - 3Count
len([8, 12, 16])produces 3. - 4Average
The program divides 36 by 3.
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.
for line in lines:
words = line.split()
print("Debug:", words)
if words[0] == "From":
print(words[2])- 1The debug output is produced immediately before the operation that can fail.
- 2If the last debug line shows an empty list, the program's input state is visible before the traceback.
- 3The 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.
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.
Debug: ['From', 'person@example.com', 'Monday']
Monday
Debug: []
IndexError: list index out of rangeGuarding 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.
for line in lines:
words = line.split()
print("Debug:", words)
if len(words) > 0:
if words[0] == "From":
print(words[2])- 1The program splits the current line into words.
- 2The debug statement shows the parsed list.
- 3The guard checks that the list contains at least one element.
- 4Only then does the program access words[0].
- 5A blank line produces [], fails the guard, and is skipped.
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 likeThe program retains total and count but later needs to identify the largest original value.
Why it failsThe individual numbers were not stored, so the original data is no longer available through that method.
Do this insteadStore 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 likeThe program evaluates words[0] immediately after line.split().
Why it failsA blank line produces an empty list, which has no index 0.
Do this insteadCheck 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 likeThe program prints general information at the start of a loop but not the parsed value before words[0].
Why it failsThe displayed state may no longer describe the input that caused the crash.
Do this insteadAdd a Debug: print immediately before the failing operation.
Common mistake Treating the traceback as a complete explanation
What it looks likeThe programmer sees IndexError and changes the index without inspecting words.
Why it failsThe traceback identifies the failure location, but the unexpected input explains why the assumption failed.
Do this insteadInspect the relevant variable, identify the malformed or empty input, and then add an appropriate guard.
Practice the Trace
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.
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()?
Reveal 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
- 1Decide whether you need only a final result or need to preserve the original values.
- 2Use total and count for a simple running average when the data will not be needed later.
- 3Use a list when you need further analysis, comparisons, or a clear view of the collected input.
- 4Use
sum()for the list total andlen()for the number of list elements. - 5When a failure occurs, print the relevant variable immediately before the failing line.
- 6If the debug output shows an empty or malformed value, compare the program's assumption with the actual input.
- 7Add a guardian condition so invalid or empty data is not used as though it were valid.
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()andlen()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.