Debugging and Logging
Investigate a failure systematically and leave useful operational evidence without overwhelming the reader.
By the end, you’ll be able to…
- Read a traceback and reduce a failing example.
- Inspect program state with a debugger.
- Use named loggers and meaningful severity levels.
01Turn a symptom into a reproducible case
Start debugging by recording the observed behavior, expected behavior, and smallest input that reproduces the difference. A vague report such as "the total is wrong" becomes actionable when one three-row input demonstrates that blank quantities are being skipped. Keep a copy of that case before editing the implementation.
Read the final exception line for its type and message, then walk the traceback frames to locate where your code supplied the bad state. The lowest relevant frame is often the immediate failure, but an earlier caller may have introduced the incorrect assumption. Do not assume the line that crashes is the line that created the bug.
02Inspect one hypothesis at a time
Form a specific hypothesis, such as "this quantity is still a string", and inspect the value and type immediately before the relevant operation. Change one factor at a time. Scattered edits followed by a successful run make it difficult to identify the cause or know whether the fix is reliable.
breakpoint() enters the configured debugger, normally pdb, in an interactive terminal. Useful pdb commands include p expression to inspect, n to execute the next line, s to step into a call, and c to continue. A breakpoint is an investigative tool; remove it before committing a noninteractive program or running an automated suite.
def total_cents(rows): total = 0 for row in rows: # In an interactive terminal, inspect p row and p type(row["cents"]). breakpoint() total += int(row["cents"]) return totalprint(total_cents([{"cents": "250"}, {"cents": "150"}]))03Log events with useful context
Logging records events that help explain a program over time. Use a named logger from logging.getLogger(__name__) in reusable modules, and let the application configure output. DEBUG suits detailed diagnostics, INFO significant normal progress, WARNING a concerning recoverable condition, and ERROR a failed operation.
Include stable context such as a record identifier and count rather than dumping an entire object. Use parameterized messages so formatting can be deferred when a level is disabled. Avoid logging passwords, tokens, or unnecessary personal data; logs often travel farther and remain longer than the original input.
04Preserve evidence without duplicating noise
Inside an exception handler, logger.exception records the message and current traceback. Use it where the application has decided how to report or terminate the failed operation. Logging the same traceback at every layer makes one failure look like several incidents and obscures the useful context.
After identifying a bug, add a regression test that fails on the original input and then apply the smallest justified fix. Confirm the test passes and check nearby behavior affected by the change. Remove temporary prints and breakpoints, retain useful permanent logging, and document any unresolved uncertainty instead of presenting an unexplained successful run as proof.
The ideas worth keeping.
- Reproduce a precise symptom before changing code.
- Inspect values to test specific hypotheses.
- Keep useful contextual logs and preserve failure evidence at a deliberate boundary.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Fix a misleading default
A function uses row.get("quantity", 1), but blank quantity text later crashes int(). Define the policy that missing means 1 while blank is invalid, and demonstrate both cases.
Explore a worked solution
def quantity_from_row(row): if "quantity" not in row: return 1 raw = row["quantity"] if not isinstance(raw, str) or not raw.strip(): raise ValueError("present quantity must be nonblank text") quantity = int(raw) if quantity <= 0: raise ValueError("quantity must be positive") return quantityassert quantity_from_row({}) == 1assert quantity_from_row({"quantity": "2"}) == 2try: quantity_from_row({"quantity": " "})except ValueError as exc: print(exc)The fix distinguishes absent and malformed data explicitly. It addresses the input assumption rather than hiding every conversion error behind a default.
Capture a log for verification
Write process(count, logger), rejecting negative counts with ValueError and logging successful counts at INFO. Capture the log in memory.
Explore a worked solution
import ioimport loggingdef process(count, logger): if count < 0: raise ValueError("count cannot be negative") logger.info("Processed %d records", count) return countstream = io.StringIO()handler = logging.StreamHandler(stream)logger = logging.Logger("practice", level=logging.INFO)logger.addHandler(handler)assert process(3, logger) == 3assert stream.getvalue() == "Processed 3 records\n"handler.close()Injecting the logger makes the evidence inspectable. A dedicated Logger instance avoids altering application-wide logging configuration in the exercise.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Fix a misleading default
A function uses row.get("quantity", 1), but blank quantity text later crashes int(). Define the policy that missing means 1 while blank is invalid, and demonstrate both cases.
exc
Keep exploring.
The official references behind this chapter, when you want a second angle or the full details.
One chapter closer.
Reading is a start. Trying it yourself makes the connection.