Debugging Programs with Print Statements
The running total method maintains only a sum and count, discarding individual numbers after computing the average.
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.
Collected list: The list is [8, 12, 16], so the original input values are still available.
Total: sum([8, 12, 16]) produces 36.
Count: len([8, 12, 16]) produces 3.
Average: 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])
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])
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
Keeping only a running total when later analysis is required
The individual numbers were not stored, so the original data is no longer available through that method.
Fix:
Store the values in a list when you need multiple statistics, comparisons, or direct inspection of the input.Assuming every parsed line contains a word at index 0
A blank line produces an empty list, which has no index 0.
Fix:
Check that the parsed list is non-empty before accessing an index.Printing a variable too far away from the failure
The displayed state may no longer describe the input that caused the crash.
Fix:
Add a Debug: print immediately before the failing operation.Treating the traceback as a complete explanation
The traceback identifies the failure location, but the unexpected input explains why the assumption failed.
Fix:
Inspect 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
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
- Decide whether you need only a final result or need to preserve the original values.
- Use total and count for a simple running average when the data will not be needed later.
- Use a list when you need further analysis, comparisons, or a clear view of the collected input.
- Use sum() for the list total and len() for the number of list elements.
- When a failure occurs, print the relevant variable immediately before the failing line.
- If the debug output shows an empty or malformed value, compare the program's assumption with the actual input.
- Add 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() 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.