Lesson 22 of 34 · 0 of 2 exercises solved
Debugging & Reading Tracebacks
Programs fail. The skill that separates beginners from professionals is not avoiding but reading them calmly and finding the cause fast.
Read a traceback from the bottom up. The last line names the exception and its message. Above it, each "File ... line ..., in ..." entry is a frame; the lowest frame is where the error happened, the ones above are who called it. Run this and read the output before reading on.
def average(numbers):
return sum(numbers) / len(numbers)
def report(groups):
for name, scores in groups.items():
print(name, average(scores))
report({"math": [90, 85], "art": []})The most common exceptions tell you exactly what to look for. NameError: a typo or a used before assignment. TypeError: wrong kind of value, often where an object was expected. KeyError/IndexError: a lookup that does not exist. AttributeError: usually None again, or a misspelled . ValueError: right type, wrong content.
# Each of these raises a different exception. Run it, then fix them one by one.
samples = [
lambda: undefined_name,
lambda: "3" + 3,
lambda: {"a": 1}["b"],
lambda: [1, 2, 3][5],
lambda: None.upper(),
lambda: int("forty"),
]
for fn in samples:
try:
fn()
except Exception as e:
print(f"{type(e).__name__:<16} {e}")print is legitimate. The f"{x=}" form prints both the name and the value. For anything longer-lived, the logging gives you levels, timestamps and the ability to switch output off without deleting lines.
import logging
logging.basicConfig(level=logging.DEBUG, format="%(levelname)s %(funcName)s: %(message)s")
log = logging.getLogger(__name__)
def service_fee(amount, rate=0.1):
log.debug(f"{amount=} {rate=}")
if amount < 0:
log.warning("negative amount, treating as zero")
amount = 0
result = round(amount * rate, 2)
log.info(f"fee is {result}")
return result
service_fee(120)
service_fee(-5)states something you believe is true at that point in the program. When the belief is wrong, it fails loudly and early instead of letting bad data travel. breakpoint() drops you into the interactive debugger on your own machine (not in this sandbox), where n steps, p prints and c continues.
def split_bill(total, people):
assert people > 0, f"need at least one person, got {people}"
assert total >= 0, "total cannot be negative"
share = total / people
return round(share, 2)
print(split_bill(100, 3))
try:
split_bill(100, 0)
except AssertionError as e:
print("caught:", e)Common question
Does an error message mean I failed?
A traceback the chain of calls that led to an exception, ending with the exception type and message. Read it from the bottom: the message says what went wrong, the last frame says where, and the frames above show how the program got there. The type alone often names the cause: NameError for a typo, TypeError for a None where an object was expected, KeyError for a missing key.
Debugging is "something is wrong" down to "this line, with this input". Reproduce the failure with the smallest input that triggers it; print or log the values along the way; decide what you expect before you look; and once you find the fix, add a test that would have caught it.
Source: The Python tutorial, on errors and exceptions: "The last line of the error message indicates what happened."
Common mix-ups
Each of these sounds reasonable. Think about why it is wrong, then open the explanation.
A good programmer writes code that doesn't produce errors.
The first line of a traceback shows where the bug is.
print statements are unprofessional; a debugger is the only proper tool.
Practice
This should return the median of a , but it has two bugs: one crashes on even-length lists and one gives wrong answers for unsorted input. Run it, read the traceback, and fix both.
Fix the median
def median(values):
n = len(values)
middle = n / 2
if n % 2 == 1:
return values[middle]
return (values[middle - 1] + values[middle]) / 2
print(median([3, 1, 2]))
print(median([4, 1, 3, 2]))
Press Run to try your code, then Check answer to grade it.Write safe_get(data, path) that walks a nested dict/list structure following a list of keys and indexes, returning None instead of raising when any step is missing. safe_get(cfg, ["db", "hosts", 0]) should give "alpha".
Defensive lookups
cfg = {"db": {"hosts": ["alpha", "beta"], "port": 5432}}
def safe_get(data, path):
for step in path:
data = data[step]
return data
print(safe_get(cfg, ["db", "hosts", 0]))
print(safe_get(cfg, ["db", "user"]))
print(safe_get(cfg, ["db", "hosts", 5]))
Press Run to try your code, then Check answer to grade it.Check yourself
In a traceback, which frame usually points at the line that actually raised the exception?
Tip: use the ← and → keys to move between lessons.