CHAPTER 34 · INTERMEDIATE4 min read

Debugging and Logging

Investigate a failure systematically and leave useful operational evidence without overwhelming the reader.

Python 3.12+Free to readModule 8 of 13
YOUR NEXT SMALL WINS

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.

reproduce_bug.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

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.

inspect_with_pdb.pyPython
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.

structured_log_messages.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

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.

LET IT CLICK

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.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

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.

01

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.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
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.

02

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.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
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.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01The last traceback frame crashes while adding a value. What should you conclude?
02Which log message gives useful context without unnecessary input exposure?
03What should follow a confirmed bug diagnosis?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

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.

EXPECTED OUTPUT
exc
challenge.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

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.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock chapter completion.