Debugging Strategies
Lesson 8 of 8
8Debugging Strategies
The big pictureFrom confidently reading a traceback, to smart print debugging, to interactive pdb, to professional logging, the tools ordered from simplest to deepest.
What the picture shows: The highlight starts at the last line with the error type, then climbs to the line where it happened, then to whoever called it.
Map of this part
The concepts in order, click any one to jump to it:
Two ways into the same idea
Debugging is detective work: the error message is the witness statement telling you where the crime happened. Then you place small cameras (prints) to see what happened, or freeze time and search the scene step by step (the debugger). A professional detective leaves a written log for whoever comes next.
Read a Traceback from the bottom: the last line is the error type and message, and above it is the call chain. print(f"{x=}") is a quick check, and breakpoint() opens pdb where you step with n, s and p. In real code use logging with DEBUG, INFO and ERROR levels instead of prints.
Required Fundamentals
| Concept | Why it matters here |
|---|---|
| Exception Handling (the Exceptions lesson) | A traceback is the detail of an unhandled exception, understanding try/except makes it easier to read. |
| Functions (the Functions lesson) | Tracing data across several nested function calls is the core debugging skill. |
Core Concepts
Debugging isn't "guessing what's wrong" but a structured process: read the available evidence, form a hypothesis, test it with minimal intervention, and repeat until you reach the root cause, not just the symptom's surface appearance.
1Reading a Traceback
Read it bottom to top: the last line tells you the exception type and message, and the line before it tells you exactly which line of code caused it.
2Print Debugging
The fastest, simplest tool: print a variable's value at a suspicious moment to see if it's what you expect. Quick, but needs manual removal afterward.
3Interactive pdb
Instead of repeated printing, breakpoint() pauses the program at exactly that spot and gives you an interactive command line to inspect it.
4Logging Instead of Print
Permanent records with severity levels (DEBUG/INFO/WARNING/ERROR) that can be turned on or off without deleting a single line of code.
Diagram 19, debugging is a structured loop, not random guessing: every failed hypothesis adds new information bringing you closer to the real cause.
Applied Example: a Silently Crashing Average Function
Let's trace the same bug with four different tools, from simplest to deepest:
## The broken code — you can't spot the bug just by looking at it
def calculate_average(scores):
total = sum(scores)
return total / len(scores)
def process_batch(all_scores):
results = []
for student_scores in all_scores:
results.append(calculate_average(student_scores))
return results
batches = [[90, 85, 88], [70, 75], []] # the third student has no scores!
print(process_batch(batches))
1️⃣ Reading the Traceback
Traceback (most recent call last):
File "report.py", line 11, in <module>
print(process_batch(batches))
File "report.py", line 8, in process_batch
results.append(calculate_average(student_scores))
File "report.py", line 3, in calculate_average
return total / len(scores)
ZeroDivisionError: division by zero
Read it from the bottom: ZeroDivisionError at line 3 inside calculate_average. The real root cause is higher up the chain: an empty [] list originally came from batches.
Diagram 20, the same calculate_average call repeats three times with identical logic; the crash only surfaces on the third call, but the real source of the problem is higher up the chain, inside batches itself.
2️⃣ Print Debugging
def calculate_average(scores):
print("DEBUG scores =", scores) # a quick check of the actual value
total = sum(scores)
return total / len(scores)
3️⃣ Interactive pdb
def calculate_average(scores):
breakpoint() # the program pauses right here, giving you a (pdb) prompt
total = sum(scores)
return total / len(scores)
## inside pdb: type p scores to see the value, n for next line, c to continue
Inside an interactive pdb session, the key commands are: p <var> to print a variable's actual value at that exact moment, n (next) to execute the next line without stepping into called functions, s (step) to step inside a called function line by line, and c (continue) to resume normal execution until the next breakpoint or the program's end. The fundamental difference from print: you can inspect any variable that occurs to you on the spot without editing the code or restarting the program.
4️⃣ Professional Logging Instead of Printing
import logging
logging.basicConfig(level=logging.DEBUG, format="%(levelname)s: %(message)s")
def calculate_average(scores):
logging.debug(f"scores={scores}")
if not scores:
logging.warning("Empty scores list — this student will be skipped")
return 0.0
return sum(scores) / len(scores)
Worked example
average([]) crashes with ZeroDivisionError. The answer: read the last traceback line first, it says division by zero, then the line above points you to total / len(items). The fix is a check before it: if not items: return 0.Myth vs. Reality
| The myth | The reality |
|---|---|
| The first line of a traceback is always the real location of the problem | It usually only shows where the problem last "surfaced"; the root cause might be several calls higher up the chain. |
| Print debugging is a "beginner" tool you should quickly outgrow | Professional developers use it daily for quick checks, the only problem is forgetting to delete it, or relying on it exclusively for complex issues. |
| logging is just a "fancier" print with no real added value | logging adds filterable severity levels, automatic timestamps, and the ability to route output to a file or external monitoring system without changing the calling code at all. |
Where Are These Strategies Actually Used?
Production systems
Logging with different levels lets you enable more detail only while investigating an issue, without stopping the service.
Code review
Being able to read a traceback quickly saves huge amounts of time when tracing a colleague's bug report.
Debugging legacy code
pdb is an essential tool for understanding complex code you didn't write, step by step, without modifying it.
Monitoring
Structured logging records typically feed automatic alerting systems when recurring errors occur in production.
Try it yourself
Where do you start reading a traceback?
The last line gives the error type and reason, the one above says where it happened. Then you climb to see who called what.
Quick Review Flashcards
Check Your Understanding
1. In the calculate_average example above, why isn't the line with ZeroDivisionError the real root cause?
Because the function itself is logically sound, the real problem is that an empty list originally reached it from batches above. Fixing only that line (like skipping the division) treats the symptom, not preventing corrupted data from being passed in the first place.
2. What's the practical difference between logging.debug() and logging.warning()?
Both log a message, but at a different severity level. Setting level=logging.WARNING in the configuration automatically hides debug messages without deleting any code, useful for filtering out noisy detail in production while keeping only important warnings.
3. Why is pdb often preferred over scattering many print lines when tracing a complex bug across several functions?
Because pdb lets you interactively inspect everything live at each pause point (every available variable's value, stepping through line by line) without having to edit the code and add/remove print lines every time you want to check something new.
Resources for this part
Sorted by kind: start with the official docs when you want the precise reference, or with an article or video when you want the explanation first.
Resources for this lesson
Grouped by type. Start with the official docs for precision, or an article or video for the explanation.
Official docs2
Done with this lesson?
Sign in to save your progress.