Debug before measuring¶
The first run of our word counter looks plausible, yet punctuation and capitalization split one word into several results. Before timing anything, we will turn that observation into a small failing test and follow the data through the program.
Objectives
Read a traceback from the final exception back to the relevant call.
Reduce a defect to a deterministic test and inspect state with
breakpoint().Choose among assertions, logging, and short-lived diagnostic printing.
Instructor note
10 min teaching
10 min type-along
5 min exercise and discussion
Start from an observable failure¶
Run the deliberately defective implementation from the repository root:
$ cd content/episodes/code
$ cp wordcount_buggy.py wordcount_working.py
$ python -c "from wordcount_working import count_words_buggy; print(count_words_buggy('Python python, PYTHON!'))"
PowerShell users can create the working copy with Copy-Item wordcount_buggy.py wordcount_working.py. The copy is ignored by Git, so the reusable faulty example remains unchanged.
The result contains three keys instead of one. First capture the difference in a cell that remains safe to execute as part of the generated notebook:
from collections import Counter
from pathlib import Path
import sys
code_dir = next(
path.resolve()
for path in (Path("content/episodes/code"), Path("../content/episodes/code"))
if path.is_dir()
)
if str(code_dir) not in sys.path:
sys.path.insert(0, str(code_dir))
from wordcount_buggy import count_words_buggy
text = "Python python, PYTHON!"
observed = count_words_buggy(text)
expected = Counter({"python": 3})
print("observed:", observed)
print("expected: ", expected)
assert observed != expected # The deliberate defect is still present.
The final assertion is a guard for the teaching fixture: it ensures the deliberately broken implementation has not been accidentally repaired. The actual correctness oracle learners apply to their working copy is:
from collections import Counter
from wordcount_working import count_words_buggy
assert count_words_buggy("Python python, PYTHON!") == Counter({"python": 3})
Run this snippet in Python or save it temporarily as test_working.py. The resulting AssertionError gives a reproducible boundary: same input, same expected output, same failure.
Before looking at the implementation, say what the result tells you and what it does not. It tells us normalization is not reflected in the keys. It does not yet tell us whether normalization failed, its return value was ignored, or the wrong value was used later. That distinction keeps the investigation focused without turning the first guess into a conclusion.
Read tracebacks from the bottom¶
The last line names the exception. The frames above it show how execution arrived there; begin at the lowest frame belonging to your code, inspect its values, then move outward only when needed. Produce a short multi-frame traceback now:
$ python -c "from wordcount import count_words; count_words(None)"
Read it from the bottom: identify the exception type, the failing expression in count_words(), and the caller that passed None. Preserve the full traceback in a job log—copying only its last line often removes the useful context.
Assertions document internal conditions that should never be false:
assert token == token.lower(), "normalization must lowercase tokens"
Do not use assert to validate user input or files: Python can disable assertions. Raise a normal exception for those cases, as make_workload() does for invalid sizes.
In a notebook, we can inspect the two candidate values without starting an interactive debugger. Predict the output before running the cell:
from wordcount import normalize_slow
raw_token = "Python,"
token = normalize_slow(raw_token)
raw_token, token
This observation motivates the debugger session below; it does not replace
it. pdb lets us stop inside the real loop and verify which value is used on
the following line.
Type-along: follow one token¶
Open wordcount_working.py and place breakpoint() after normalization:
for raw_token in text.split():
token = normalize_slow(raw_token)
breakpoint()
if token:
counts[raw_token] += 1
Then run the small example again. At the (Pdb) prompt:
p raw_token
p token
n
c
p evaluates an expression, n advances to the next line, and c continues. The evidence is now concrete: token is normalized, but the dictionary indexes raw_token. Replace counts[raw_token] with counts[token], remove the breakpoint, and rerun the oracle.
For a non-interactive batch failure, first preserve inputs and logs, reproduce with the smallest case, and then use python -m pdb script.py in an interactive allocation. An attached debugger cannot help after a batch job has disappeared unless the evidence was recorded.
Logging and IceCream¶
Use logging for durable, levelled diagnostics:
import logging
logger = logging.getLogger(__name__)
def log_token_metadata(raw_token: str) -> None:
logger.debug("normalizing a token with %d characters", len(raw_token))
logging.basicConfig(level=logging.DEBUG)
log_token_metadata("Python,")
Avoid logging credentials, access tokens, personal data, proprietary inputs, or huge arrays. Log shape, type, range, or an anonymized identifier instead.
IceCream is an optional convenience for a short local investigation. ic(expression) prints the expression, its value, and context to standard error, and returns the value. Try the bounded example:
$ python debug_with_icecream.py
It is not a debugger or production logging system. Do not use global installation helpers in shared course code; remove or disable calls before benchmarking, and never pass secrets or large scientific data to ic().
After completing the exercise, remove test_working.py if you created it. You may keep the ignored wordcount_working.py for reference or delete it; later repository tests continue to validate the untouched course examples.
Exercise
Without running the code, predict which value should index counts: raw_token or token. Then use pdb to collect one observation that supports your answer.
Solution
token must be the key because it holds the normalized value. For input Python,, p raw_token shows 'Python,' while p token shows 'python'; indexing by the former discards the completed normalization.
Keypoints
Establish correctness with a small reproducible case before measuring performance.
A debugger follows state and control flow; logging preserves selected evidence across runs.
Diagnostic output can expose sensitive data and distort timings.
See also
Python documents pdb, logging, and exceptions.