Python Part I · Module 14 · Errors and Exception Handling
The Errors Beginners Actually Hit
There are dozens of built-in exception types and you will meet about six of them. What makes them worth learning is not the names — the names are on the last line of the traceback and you can read — but the fact that each one is a diagnosis. A KeyError is not merely a missing key; it is evidence that the data did not have the shape your code assumed, which is a different bug from the one a ValueError indicates, which is different again from a TypeError even though the two are constantly confused. This chapter is the six types, each as a program that genuinely fails, and for each one the inference you are entitled to draw the moment you see it.
Foundation17 min readPythonPart IFundamentalsErrorsDebugging
Part I · Module 14 · Chapter 14.3 · The Errors Beginners Actually Hit
1. An Error Type Is a Diagnosis
The last line of a traceback has two halves, and both carry information:
ValueError: invalid literal for int() with base 10: 'n/a'The message tells you about this specific failure: the offending text was
'n/a'. The type tells you about the category of mistake, and that is the
half people skim. Three lines from the canonical examples:
int("n/a") # ValueError - right type, impossible content
result["runtime"] # KeyError - the dict did not have the shape you assumed
tests[9] # IndexError - the sequence was shorter than you thoughtEach comment is a hypothesis about what you did wrong, available before you have looked at a single line of your own code. That is what makes the six worth knowing.
Every example below is a real program that really fails. No exception is caught anywhere in this chapter — catching is Chapter 14.4 — so each one is also more practice at reading a traceback bottom-up.
2. NameError — the name does not exist here
runtime_s = 182.4
print(f"runtime: {runtime_S}")Traceback (most recent call last):
File "nameerror.py", line 2, in <module>
print(f"runtime: {runtime_S}")
^^^^^^^^^
NameError: name 'runtime_S' is not definedThe diagnosis: you used a name that was never bound, or bound somewhere this code cannot see. In practice it is one of four things:
- a typo or a case difference (
runtime_Sforruntime_s— Python is case-sensitive, as Chapter 2.2 noted); - a name defined inside a function and used outside it (Chapter 11.10);
- a module you forgot to
import(Chapter 13.2); - a name defined later in the file than the line that uses it.
When the name is close to one Python knows, you get a suggestion for free:
tests = ["smoke", "cdc"]
print(lenn(tests))Traceback (most recent call last):
File "suggestion.py", line 2, in <module>
print(lenn(tests))
^^^^
NameError: name 'lenn' is not defined. Did you mean: 'len'?3. TypeError — the operation does not apply to that type
runtime = 182.4
print("runtime: " + runtime)Traceback (most recent call last):
File "typeerror.py", line 2, in <module>
print("runtime: " + runtime)
~~~~~~~~~~~~^~~~~~~~~
TypeError: can only concatenate str (not "float") to strThe diagnosis: the types themselves are incompatible. Not the values — no
float whatsoever could be concatenated to a string. The fix is a conversion or
an f-string (Chapter 4.12), which is why f"runtime: {runtime}" is the
idiomatic form.
The most common TypeError in real scripts is subtler and comes from a function
that returned nothing:
def total_runtime(values):
"""Forgot to return — so it returns None."""
sum(values)
t = total_runtime([182.4, 204.1])
print(t + 10)Traceback (most recent call last):
File "nonetype.py", line 7, in <module>
print(t + 10)
~~^~~~
TypeError: unsupported operand type(s) for +: 'NoneType' and 'int'Any error mentioning NoneType should send you looking for a function that
forgot to return, which is exactly the trap Chapter 11.8 was about. The
failing line is line 7; the bug is on line 3.
4. ValueError — right type, impossible content
This is the one that gets confused with TypeError, and the distinction is
clean:
print(int("182"))
print(int("n/a"))182
Traceback (most recent call last):
File "valueerror.py", line 2, in <module>
print(int("n/a"))
~~~^^^^^^^
ValueError: invalid literal for int() with base 10: 'n/a'int accepts strings — line 1 proves it. So the type was fine; this
particular string could not be interpreted as a number. That is a ValueError.
Compare directly:
print(int("n/a"))
print(int(["182"]))Traceback (most recent call last):
File "both.py", line 1, in <module>
print(int("n/a"))
~~~^^^^^^^
ValueError: invalid literal for int() with base 10: 'n/a'Only the first failure appears, because the program stops there. Swap the lines and you get the other:
print(int(["182"]))Traceback (most recent call last):
File "typefirst.py", line 1, in <module>
print(int(["182"]))
~~~^^^^^^^^^
TypeError: int() argument must be a string, a bytes-like object or a real number, not 'list'
TypeErrormeans "I do not accept that kind of thing at all."ValueErrormeans "I accept that kind of thing, but not that one."
The diagnosis for ValueError: your data is dirtier than your code assumes.
It is the signature error of parsing anything a tool produced — a dash where a
number should be, an empty field, a N/A.
Another common form has nothing to do with parsing:
row = "smoke PASS 182.4".split()
name, status = rowTraceback (most recent call last):
File "unpack.py", line 2, in <module>
name, status = row
^^^^^^^^^^^^
ValueError: too many values to unpack (expected 2)Three fields, two names. Chapter 11.9 used this shape deliberately; here it is as a failure.
5. IndexError — the sequence was shorter than you thought
tests = ["smoke", "cdc"]
print(tests[0])
print(tests[1])
print(tests[2])smoke
cdc
Traceback (most recent call last):
File "indexerror.py", line 4, in <module>
print(tests[2])
~~~~~^^^
IndexError: list index out of rangeThe diagnosis: an off-by-one, or data that was shorter than expected. The two cases look identical and the fix differs completely — one is a loop bound, the other is a missing input line — so the next question is always how long is it actually?
The specific real-world form is a line with fewer fields than the format promised:
for line in ["smoke PASS 182.4", "reset FAIL"]:
fields = line.split()
print(fields[0], fields[2])smoke 182.4
Traceback (most recent call last):
File "shortrow.py", line 3, in <module>
print(fields[0], fields[2])
~~~~~~^^^
IndexError: list index out of rangeNote that IndexError says nothing about which index — the message is always
list index out of range. The caret line is what tells you it was fields[2].
6. KeyError — the dictionary did not have that key
result = {"test": "smoke", "status": "PASS", "runtime_s": 182.4}
print(result["runtime"])Traceback (most recent call last):
File "keyerror.py", line 2, in <module>
print(result["runtime"])
~~~~~~^^^^^^^^^^^
KeyError: 'runtime'The message is just the key — no explanation, because none is needed.
The diagnosis: the data did not have the shape your code assumed. Either the
key is spelled differently (runtime_s, as it is here), or the dictionary came
from somewhere that omits it for some records. The second case is the dangerous
one: it works for thousands of records and fails on one.
Chapter 8.3 gave the two tools for this — in and .get() — and they remain
the right answer whenever a key is genuinely optional:
result = {"test": "smoke", "status": "PASS"}
print(result.get("runtime_s", "not recorded"))
print("runtime_s" in result)not recorded
False7. FileNotFoundError — the path is not there
f = open("sim.log")Traceback (most recent call last):
File "missing.py", line 1, in <module>
f = open("sim.log")
FileNotFoundError: [Errno 2] No such file or directory: 'sim.log'Files are Module 15's subject and open is explained properly there; it appears
here only because it is the thing that raises this error.
The diagnosis: the path is wrong, or you are not where you think you are.
The second is far more common than people expect — a relative path like
sim.log is resolved from the working directory, not from the script's
directory, exactly as Chapter 13.5 Section 6 found for imports. The same script
run from two places looks for two different files.
Note also [Errno 2]: the operating system reported this, not Python.
8. The Two That Are Not About Your Data
ModuleNotFoundError is a NameError for files:
import panadasTraceback (most recent call last):
File "nomodule.py", line 1, in <module>
import panadas
ModuleNotFoundError: No module named 'panadas'The diagnosis: a typo, something not installed, or a path problem — in that order of likelihood. Chapter 13.1 covered where Python looks.
AttributeError means you asked an object for something it does not have:
name = "smoke"
name.push("_test")Traceback (most recent call last):
File "attrerror.py", line 2, in <module>
name.push("_test")
^^^^^^^^^
AttributeError: 'str' object has no attribute 'push'The message names the type first, and that is the useful half — 'str' object tells you what Python thinks it is holding, which is often the surprise.
The overwhelmingly common version in real scripts:
def find_status(lines):
"""Returns None when nothing matches — and nothing matches here."""
for line in lines:
if line.startswith("STATUS"):
return line
status = find_status(["UVM_INFO ...", "UVM_INFO ..."])
print(status.strip())Traceback (most recent call last):
File "nonattr.py", line 9, in <module>
print(status.strip())
^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'strip''NoneType' object has no attribute is one of the most common errors in all
of Python, and it always means the same thing: something returned None and
you used the result as though it had not. A function with no matching case, a
.get() that missed, or a function that forgot to return.
9. Predict, Run, Observe
Four statements. Name the error type for each before reading on:
data = {"test": "smoke", "runtime_s": "182.4"}
print("A:", data["runtime_s"] + 10)Traceback (most recent call last):
File "predict.py", line 3, in <module>
print("A:", data["runtime_s"] + 10)
~~~~~~~~~~~~~~~~~~^~~~
TypeError: can only concatenate str (not "int") to strA is a TypeError, not a ValueError. The dictionary stored the runtime as
a string — note the quotes in the literal. Adding an int to a str is a type
mismatch; no values of those two types would work.
data = {"test": "smoke", "runtime_s": "n/a"}
print("B:", float(data["runtime_s"]))Traceback (most recent call last):
File "predict2.py", line 3, in <module>
print("B:", float(data["runtime_s"]))
~~~~~^^^^^^^^^^^^^^^^^^^
ValueError: could not convert string to float: 'n/a'B is a ValueError. float accepts strings; it does not accept this
string. The same expression with "182.4" works, which is the definition of a
value problem rather than a type problem.
Notice the two messages are different in shape as well: int() says
invalid literal for int() with base 10, float() says
could not convert string to float. Both are ValueError.
10. The Card
| Type | Means | Look for |
|---|---|---|
NameError | the name is not bound here | typo, case, scope, missing import |
TypeError | the operation does not apply to that type | a None from a function that forgot to return; a str where a number was meant |
ValueError | right type, impossible content | dirty input — N/A, empty field, a dash |
IndexError | the sequence is shorter than assumed | off-by-one, or a short line |
KeyError | the dict lacks that key | spelling, or a record of a different shape |
FileNotFoundError | the path is not there | wrong path, or wrong working directory |
AttributeError | the object has no such name | NoneType means something returned None |
ModuleNotFoundError | the import target is not findable | typo, not installed, path |
Two shortcuts worth memorising outright:
NoneTypeanywhere → find the function that did not return.TypeErrorversusValueError→ ask whether any value of that type would have worked. If yes, it is aValueError.
11. Exercises
Exercise 1 — produce all six. Write six one-or-two-line files, each raising a different type from the card. Keep them; they are a useful reference.
Exercise 2 — the distinction. Write two calls to int(), one raising
TypeError and one raising ValueError. State the rule that separates them in
your own words.
Exercise 3 — the None hunt. Write a function that returns None on one
input, then use its result two different ways so that one produces a TypeError
and the other an AttributeError. Say what both have in common.
Exercise 4 — short line. Parse a list of log lines where one has fewer fields than the rest. Record the error and the caret line, and say which index failed.
Exercise 5 — the optional key. Take the KeyError example and fix it twice:
once with in, once with .get(). Then say when fixing it that way is the
wrong thing to do.
Exercise 6 — working directory. Produce a FileNotFoundError with a relative
path, then make the identical script succeed by running it from elsewhere.
Explain in one sentence what that proves about relative paths.
Exercise 7 — read the type, not the message. For each, name the type without
running it: (a) "5" * "5" (b) int("5.5") (c) [1,2][2] (d) {"a":1}["b"]
(e) None.strip() (f) improt json.
Exercise 8 — diagnose from the type alone. A colleague says only "it threw a
KeyError on the nightly run but works on my machine". Write the three
questions you would ask, each justified by what KeyError licenses you to
infer.
12. Summary
- An error type is a diagnosis, not a label — it narrows the cause before you read any of your own code.
NameError— the name is not bound where you used it: typo, case, scope, or missing import.TypeError— the operation does not apply to that type at all.ValueError— the type was right and the content was not; the signature error of parsing real tool output.- The test that separates them: would any value of that type have worked?
If yes, it is a
ValueError. IndexError— the sequence was shorter than assumed; the message never says which index, so read the caret line.KeyError— the data did not have the shape your code assumed; the message is just the key.FileNotFoundError— wrong path, or a relative path resolved from a working directory you did not expect.AttributeErrormentioningNoneTypeis one of the most common errors in Python, and always means something returnedNone.- Suppressing a
KeyErrorwith.get()hides a question when the key was supposed to be there.
You can now classify a failure and find it. The rest of the module is about deciding what your script should do about it — starting with the observation that the right answer is often nothing at all.
Where this fits
Part of the Python curriculum.
