Getting a program to run
Reading a Traceback Without Panicking
A traceback is a map, not a verdict. Learn to read it from the bottom up, and to tell the difference between where the program died and where the mistake was made.
You run a script, the terminal fills with red text, and the first instinct is to scroll to the top and start reading. That is the worst place to start. The top of a traceback is where the program began. The bottom is where it stopped, and the reason it stopped is written there in plain English.
The people who appear to debug quickly are not reading faster. They are reading in a different order, and they have stopped treating the traceback as an accusation. It is a recording of a call stack at the moment something went wrong, and every line in it is a fact you can use.
The last line names the failure
Take a file called report.py that does not work:
def load_rows(path):
with open(path) as handle:
return [line.strip() for line in handle]
def summarise(rows):
return sum(int(row) for row in rows)
rows = load_rows("numbers.txt")
print(summarise(rows))
Run it with a numbers.txt that contains a blank line somewhere in the
middle, and you get this:
Traceback (most recent call last):
File "/home/ana/report.py", line 11, in <module>
print(summarise(rows))
File "/home/ana/report.py", line 7, in summarise
return sum(int(row) for row in rows)
File "/home/ana/report.py", line 7, in <genexpr>
return sum(int(row) for row in rows)
ValueError: invalid literal for int() with base 10: ''
The bottom line tells you three things: the exception type is
ValueError, the message is invalid literal for int() with base
10: '', and the value it choked on is the empty string. You now know more than
you did, and you have not read a single frame.
The phrase invalid literal for int() is worth internalising because it
is one of the most common messages in Python. It means int() was handed
something that is not a number in string form. The colon and the quotes at the end tell
you what that something was.
The frames read bottom to top
Above the exception line are the frames, and they are in the opposite order to what you want. The first frame is the outermost caller. The last frame is where execution actually stopped. Read them upward:
line 11, in <module>calledsummarise(rows).line 7, in summariseran a generator expression overrows.line 7, in <genexpr>attemptedint(row)and failed.
The <module> label means top-level code, the part of the file that
is not inside a function. A <genexpr> frame is a generator
expression, which is why line 7 appears twice: the generator is defined on that line
and also executes on it.
That double appearance confuses people the first time. The rule is that each frame is one function call, and a generator expression or comprehension is a function for this purpose even though it does not look like one.
The frame that raised is not always the bug
int('') is where the program died, not where the mistake was made.
The mistake is in load_rows, which returned a blank string as though it
were data. Fixing the failure site means adding a guard in summarise
that hides the real problem for every future caller.
Exception types have shapes you can recognise
Three exception types account for most of what a beginner sees, and each one has a distinct signature. Knowing the shape tells you which layer of the program to look at before you read a single frame.
| Exception | What it means | Where to look |
|---|---|---|
NameError |
A name is used that has never been assigned, or is assigned after this line. | Spelling, and definition order in the file |
TypeError |
A value of the wrong type reached an operation: adding a string to an int, calling something that is not callable. | The caller that passed the value in |
AttributeError |
An attribute does not exist on that object, often because the object is None. |
The line that produced the object |
KeyError |
A dictionary lookup for a key that is not present. | The data, not the code |
IndentationError |
The file does not parse at all, so nothing ran. | The line number in the message |
IndentationError is the odd one out: it happens before your code
executes, so the traceback is minimal and the line number points at the offending
indentation directly. Mixing tabs and spaces produces
TabError, which is a subclass and prints similarly.
An AttributeError that is really a None
This is the failure that costs beginners the most time, because the message names a line that looks correct:
def find_customer(customers, customer_id):
for customer in customers:
if customer["id"] == customer_id:
return customer
customer = find_customer([{"id": 1}], 2)
print(customer["email"].lower())
Traceback (most recent call last):
File "lookup.py", line 8, in <module>
print(customer["email"].lower())
AttributeError: 'NoneType' object has no attribute 'lower'
Read the message as a sentence about a type, not about your code. It says: the object
you called .lower() on is None. That is a fact about a value.
The question it raises is the useful one: why is this None?
The answer is that find_customer returned nothing when the loop
completed without a match, and a function with no explicit return returns
None. The bug is a missing branch, not a bad method call. Guarding the
call site with if customer is not None would silence the crash and leave a
function that silently returns nothing whenever it is asked for something absent.
Use the traceback to choose the fix
Once you can read the shape, the traceback answers a question you should ask before editing anything: is the code wrong, or is the input wrong?
ValueError: invalid literal for int() on a line reading a user-supplied
file is an input problem, and the fix belongs where the file is read.
TypeError: unsupported operand type(s) for +: 'int' and 'str' inside your
own function is a code problem. KeyError on a configuration dictionary is
almost always a deployment problem: the code is right and the environment is missing a
value.
Getting this wrong in either direction is expensive. Treating an input problem as a code problem produces defensive checks scattered through logic that was correct. Treating a code problem as an input problem produces a validation layer that rejects good data and never fixes the bug.
Create a file containing exactly this, run it, and read the traceback before you change anything.
def average(values):
return sum(values) / len(values)
def report(scores):
for name, values in scores.items():
print(name, average(values))
report({"ana": [8, 9], "bo": [], "cy": [7]})
Write down three things before you touch the code: the exception type, the line where it was raised, and the function that supplied the bad value.
Then decide whether the fix belongs in average,
report, or the caller. There is more than one defensible answer, and
the reasoning is what matters.
Show the solution and the reasoning
The failure is ZeroDivisionError: division by zero, raised inside
average, and the value that caused it came from report,
which passed an empty list. The traceback shows three frames:
<module>, then report, then average.
Three fixes are reasonable and they mean different things.
Return None for an empty sequence and let the caller decide:
def average(values):
if not values:
return None
return sum(values) / len(values)
This makes "no data" a value the caller must handle. It is the right choice when an empty input is a legitimate state, and it moves the decision to the layer that knows what an empty average should mean.
Raise a domain exception so the failure is explicit:
def average(values):
if not values:
raise ValueError("average() needs at least one value")
return sum(values) / len(values)
This is better when an empty list reaching average indicates a bug
upstream. The message is more useful than division by zero because it
names the contract that was broken.
Skip empty rows in report and print a placeholder:
def report(scores):
for name, values in scores.items():
result = average(values)
print(name, "no scores" if result is None else result)
This is the right answer when the report should be complete: every name appears, and the ones without data say so. It is the wrong answer if an empty list means something has gone wrong earlier and should not be tolerated.
What is not defensible is wrapping the division in try and
except ZeroDivisionError: pass. That converts a visible failure into a
report that quietly omits information, and the next person to debug it starts from
nothing.
Progress
Mark this lesson done and it stays marked in this browser.