Course outline · 0% complete

0/29 lessons0%

Course overview →

The Exception Bestiary

lesson 8-2 · ~10 min · 24/29

Each exception type is a hint

The exception type on that last line narrows the hypothesis before you read any code. The six you will meet daily:

ExceptionUsual meaningFirst place to look
TypeErrorvalue of the wrong type used in an operationmixing str and int, or a variable that is None
ValueErrorright type, unacceptable contentparsing: int("12px")
KeyErrordictionary key does not existtypo in the key, or data missing a field
IndexErrorlist index past the endoff-by-one, or an empty list
AttributeErrorobject has no such attribute or methodtypo, wrong type, or a method that returned None
NameErrorvariable or function name not definedtypo, or using a name before defining it

One trap earns a special note: sort(), append(), and friends modify the list and return None. Save their return value and the next line blows up with a confusing NoneType message.

The bestiary in captivity

Five classic mistakes, each caught, printing its exception type and message. Match each line against the table above.

examples = [
    ("int('12px')", lambda: int("12px")),
    ("[1, 2][5]", lambda: [1, 2][5]),
    ("{'a': 1}['b']", lambda: {"a": 1}["b"]),
    ("len(42)", lambda: len(42)),
    ("'hi'.push('!')", lambda: "hi".push("!")),
]

for label, attempt in examples:
    try:
        attempt()
    except Exception as e:
        print(f"{label}  ->  {type(e).__name__}: {e}")

Output

int('12px')  ->  ValueError: invalid literal for int() with base 10: '12px'
[1, 2][5]  ->  IndexError: list index out of range
{'a': 1}['b']  ->  KeyError: 'b'
len(42)  ->  TypeError: object of type 'int' has no len()
'hi'.push('!')  ->  AttributeError: 'str' object has no attribute 'push'

Each attempt is wrapped in a lambda so the failure happens inside the loop rather than while building the list. Writing ("int('12px')", int("12px")) would crash on the first line and never reach the try, which is a real and confusing mistake when collecting examples like this.

type(e).__name__ extracts the exception class name as a string, which is how the output labels each row. except Exception catches all five different types, and that breadth is appropriate for a demonstration and wrong in production code, where catching only the type you can handle is the rule.

Two of the messages quote the guilty value directly, being '12px' and 'b', and two name the guilty type, being 'int' and 'str'. Noticing which style a message uses tells you whether to go looking for a bad value or a bad variable.

The push example is a deliberate cross-language slip, since push is JavaScript's method and Python's is append. AttributeError is what a wrong-language reflex produces, and it is also what a plain typo produces.

The type behind a NoneType message

When nums = nums.sort is followed by nums[0] crashing with 'NoneType' object is not subscriptable, the exception type is TypeError.

sort() sorts in place and returns None, so nums became None, and subscripting None uses a value of the wrong type in an operation, which is the definition of a TypeError.

There are two fixes and they differ in intent:

  • nums.sort() with no assignment, which reorders the existing list
  • nums = sorted(nums), which returns a new list and leaves the original alone

Choosing between them is a real decision rather than a style preference. If the caller still needs the original order, sorted is required, and if the list is large and the original order is dead, sorting in place avoids a copy.

The reason this trap is worth memorizing is that the crash lands one line after the mistake. The assignment succeeds quietly, and the subscript is what reports the problem, so the traceback points at an innocent line and the guilty one is above it.

The sort trap, live

top_three crashes with TypeError: 'NoneType' object is not subscriptable. Before the fix, it assigned the result of scores.sort(reverse=True).

def top_three(scores):
    ordered = sorted(scores, reverse=True)
    return ordered[:3]

print(top_three([70, 95, 88, 60, 91]))

Output

[95, 91, 88]

scores.sort(reverse=True) sorts the list in place and returns None, so ordered is None and the slice on the next line fails. sorted(scores, reverse=True) returns a new sorted list you can slice, which is the one-function swap that fixes the whole thing.

sorted with a keyword argument is from Advanced Python, and reverse=True works identically on both the method and the function, which is part of why the two are so easy to confuse.

Using sorted here has a second benefit beyond not crashing. The caller's list is left in its original order, so a function named top_three no longer silently rearranges the data it was handed, and that kind of hidden side effect is a bug waiting for its second caller.

The slice [:3] is worth a glance too, since it returns as many elements as exist rather than raising on a short list. top_three([5]) gives [5], so there is no IndexError to guard against here, which is the opposite of the empty-input problem from lesson 3-1.

Diagnosing a NoneType attribute error

A trace ending with AttributeError: 'NoneType' object has no attribute 'strip' supports one strong hypothesis: some earlier call returned None instead of a string, and it flowed here.

NoneType in a message almost always means a None leaked in upstream. The usual sources are worth knowing by heart:

  • a function with no return statement, or one whose return sits inside an if that did not run
  • a failed lookup with dict.get(), which returns None rather than raising
  • the sort() trap from the previous block
  • a regular-expression search or a find-style helper that returns None when nothing matches

Note that the line calling .strip() is almost never the bug. It is a perfectly reasonable thing to do to a string, and the mistake is that the value is not a string, so walking up the traceback chain in lesson 8-1 style is what finds the producer of the None.

The fix belongs at the source in most cases. Guarding the call site with if value is not None hides the leak and leaves the same None to surface somewhere else later, which turns one clear crash into several vague ones.

Reading a quoted value out of a ValueError

A trace ending with ValueError: invalid literal for int with base 10: ' 42px' hands you this hypothesis directly: the string ' 42px' reached the conversion carrying a space and units, so whatever produced it needs cleaning before int().

ValueError means the type was right and the content was unacceptable, and this message goes further by quoting the guilty content. The leading space and the px suffix together say the input was never cleaned, which is much more specific than knowing a conversion failed.

The quoting is what makes the space visible, which is the same argument for repr from lesson 7-3. Printed bare, ' 42px' and '42px' look nearly identical, and the difference decides whether strip() alone is enough.

Your next move is lesson 8-1's, walking up the chain to find who produced ' 42px', then stripping or parsing it at the source. A value shaped like that usually came from a form field, a scraped page, or a CSV column, and all three want cleaning once at the boundary rather than at every use.

Worth noting what the fix is not. Wrapping the int() call in a try and defaulting to 0 makes the crash go away and silently turns a real number into a wrong one, which is the quiet failure that lesson 3-1 warned about being worse than the loud one.