Kodewick
Lesson 21 of 31Contents

Errors and exceptions

Lesson 21 of 31 · Part 4 · Code and data · about 20 minutes

Some mistakes stop a program before it starts: a missing quote is a SyntaxError, and Python refuses to run anything. Others only show up while it runs. Turning "ten" into a number is fine to write, but it cannot be done, so Python stops at that line and raises an exception:

main.py
1print("Reading the meter")2lamps = int("ten")3print("Lamps:", lamps)
Output
Reading the meter
Traceback (most recent call last):
  File "main.py", line 2, in <module>
    lamps = int("ten")
ValueError: invalid literal for int() with base 10: 'ten'

The first line ran; the third never did. An exception is Python saying "I can't go on from here". This lesson shows how to read one, and how to catch it so your program can carry on.

Reading a traceback

When the error happens inside a function, the report is longer. Here report calls cost_per_house, and the second street has no houses:

main.py
1def cost_per_house(lamps, houses):2    return lamps * 4 / houses3 4def report(street, lamps, houses):5    cost = cost_per_house(lamps, houses)6    print(street, cost)7 8report("Elm Street", 6, 3)9report("Oak Lane", 2, 0)
Output
Elm Street 8.0
Traceback (most recent call last):
  File "main.py", line 9, in <module>
    report("Oak Lane", 2, 0)
  File "main.py", line 5, in report
    cost = cost_per_house(lamps, houses)
  File "main.py", line 2, in cost_per_house
    return lamps * 4 / houses
ZeroDivisionError: division by zero

That report is called a traceback. It lists every call that was still running, outermost first: line 9 called report, line 5 inside report called cost_per_house, and line 2 is where it went wrong. The last line names the exception and says what happened. Read it from the bottom up: what went wrong, where, then how the program got there.

These are the exceptions you will meet most:

ExceptionRaised whenExample
ValueErrorthe type is right, the value is notint("ten")
TypeErrorthe type is wrong for the operation"lamp" + 1
ZeroDivisionErrordividing by zero5 / 0
IndexErrora list position that does not exist[1, 2][5]
KeyErrora dictionary key that does not exist{"ore": 3}["oil"]
NameErrora name that was never definedprint(lamsp)

try and except

Put the risky code in a try block. If an exception happens there, Python jumps to the except block instead of stopping. If nothing goes wrong, the except block is skipped.

main.py
1for text in ["12", "ten", "7"]:2    try:3        lamps = int(text)4        print("Got", lamps)5    except ValueError:6        print("Not a number:", text)7print("Done")
Output
Got 12
Not a number: ten
Got 7
Done

"ten" fails at int(text), so print("Got", lamps) is skipped for it, and the loop goes on to "7". Name the exception you expect after except. An except ValueError catches only that kind; any other exception still stops the program, which is what you want for mistakes you did not plan for.

Tip: Press Step and watch where Python goes the second time through the loop: from the int line straight to except.

The error message

Add as and a name to get the exception itself. Printing it gives the message from the last line of a traceback, the text after the exception's name:

main.py
1try:2    lamps = int("ten")3except ValueError as e:4    print("Problem:", e)5    print("Kind:", type(e).__name__)
Output
Problem: invalid literal for int() with base 10: 'ten'
Kind: ValueError

This is useful when you log what went wrong, or show it to the person using the program. The name e is only a habit; any name works.

Several kinds of error

One try can have several except blocks, one per kind of exception. Python checks them from the top and runs the first that matches. To handle a few kinds the same way, put them in parentheses.

main.py
1stock = {"ore": 12, "oil": 0}2 3def per_crate(item, crates):4    try:5        return stock[item] // crates6    except KeyError:7        return "no such item"8    except (ZeroDivisionError, TypeError):9        return "bad crate count"10 11print(per_crate("ore", 4))12print(per_crate("wood", 4))13print(per_crate("ore", 0))14print(per_crate("ore", "four"))
Output
3
no such item
bad crate count
bad crate count

except Exception: catches almost every kind at once. Use it sparingly: it also hides typos like a misspelled variable name, which you would rather see.

Guess first

What does this print?

1try:2    print("A")3    x = [1, 2, 3][3]4    print("B")5except KeyError:6    print("C")7except IndexError:8    print("D")9print("E")

else and finally

Two more blocks can follow. else runs only if the try block finished without an exception. finally runs every time, error or not, and is the place for cleanup that must happen.

main.py
1def light(text):2    try:3        lamps = int(text)4    except ValueError:5        print(text, "is not a number")6    else:7        print("Lighting", lamps, "lamps")8    finally:9        print("Ladder put away")10 11light("3")12light("three")
Output
Lighting 3 lamps
Ladder put away
three is not a number
Ladder put away
BlockRuns when
tryalways first
exceptan exception of that kind happened in try
elsetry finished with no exception
finallyalways, last

Keeping only the risky line in try and the rest in else means an unexpected error in the rest is not caught by mistake.

Checking input

input() always gives back a string, and people type anything (lesson 6). A while True loop with try asks again until the answer is usable. This example is given three answers in turn:

main.py
1while True:2    try:3        lamps = int(input("How many lamps? "))4    except ValueError:5        print("Please type a whole number.")6        continue7    if lamps < 0:8        print("That can't be below zero.")9        continue10    break11print("Lighting", lamps, "lamps")
Output
How many lamps? lots
Please type a whole number.
How many lamps? -2
That can't be below zero.
How many lamps? 5
Lighting 5 lamps

int catches text that is not a number; the if catches a number that makes no sense. Only when both checks pass does break leave the loop.

raise and your own exceptions

Your own functions can raise exceptions too. raise stops the function and sends the exception to whoever called it, just like Python's built-in ones:

main.py
1def set_brightness(level):2    if not 0 <= level <= 100:3        raise ValueError(f"brightness must be 0-100, not {level}")4    print("Brightness", level)5 6set_brightness(80)7try:8    set_brightness(150)9except ValueError as e:10    print("Refused:", e)11set_brightness(-5)
Output
Brightness 80
Refused: brightness must be 0-100, not 150
Traceback (most recent call last):
  File "main.py", line 11, in <module>
    set_brightness(-5)
  File "main.py", line 3, in set_brightness
    raise ValueError(f"brightness must be 0-100, not {level}")
ValueError: brightness must be 0-100, not -5

The caller decides what to do: the try turns the second call into a message, while the third call, with no try around it, stops the program.

You can also make a kind of exception of your own, so callers can catch exactly your problem and nothing else. It takes two lines; lesson 26 explains class, and lesson 27 what the (Exception) part means.

main.py
1class LampError(Exception):2    pass3 4def change_bulb(lamp, bulbs_left):5    if bulbs_left == 0:6        raise LampError(f"no bulbs left for {lamp}")7    return bulbs_left - 18 9bulbs = 110try:11    bulbs = change_bulb("lamp 1", bulbs)12    bulbs = change_bulb("lamp 2", bulbs)13except LampError as e:14    print("Stopped:", e)15print("Bulbs left:", bulbs)
Output
Stopped: no bulbs left for lamp 2
Bulbs left: 0

Practice

Write the code, then press Check. A hint costs nothing; the answer is there when you need it.

Exercise 1

Safe division

Finish share(ore, miners) so it returns each miner's share with //, or the string "no miners" when miners is 0. Use try and except, not if.

It should print
6
no miners
exercise1.py
1def share(ore, miners):2    return ore // miners3 4print(share(20, 3))5print(share(20, 0))
Output
Press Run or Ctrl+Enter
Exercise 2

Skip the bad readings

Add up the readings that are whole numbers and count the ones that are not. Print the total, then the number skipped.

It should print
Total: 13
Skipped: 3
exercise2.py
1readings = ["4", "x", "7", "", "2", "3.5"]2total = 03skipped = 04for r in readings:5    total += int(r)6print("Total:", total)7print("Skipped:", skipped)
Output
Press Run or Ctrl+Enter
Exercise 3

Raise it yourself

Make load_cart(weight) raise a ValueError with the message too heavy: 120 (the weight in the message) when the weight is over 100. The loop already catches it and prints the message.

It should print
Loaded 40
Error: too heavy: 120
Loaded 90
exercise3.py
1def load_cart(weight):2    print("Loaded", weight)3 4for w in [40, 120, 90]:5    try:6        load_cart(w)7    except ValueError as e:8        print("Error:", e)
Output
Press Run or Ctrl+Enter

What you learned

  • An exception stops the program at the line where it happens; the traceback lists the calls, outermost first, and its last line says what went wrong.
  • try / except SomeError: catches that kind of exception so the program can carry on; as e gives you the message.
  • Several except blocks handle different kinds; parentheses handle several kinds the same way.
  • else runs when nothing went wrong; finally runs every time.
  • raise ValueError("...") raises an exception from your own code; class MyError(Exception): pass makes a kind of your own.