Errors and exceptions
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:
1print("Reading the meter")2lamps = int("ten")3print("Lamps:", lamps)
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:
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)
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 zeroThat 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:
| Exception | Raised when | Example |
|---|---|---|
ValueError | the type is right, the value is not | int("ten") |
TypeError | the type is wrong for the operation | "lamp" + 1 |
ZeroDivisionError | dividing by zero | 5 / 0 |
IndexError | a list position that does not exist | [1, 2][5] |
KeyError | a dictionary key that does not exist | {"ore": 3}["oil"] |
NameError | a name that was never defined | print(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.
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")
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:
1try:2 lamps = int("ten")3except ValueError as e:4 print("Problem:", e)5 print("Kind:", type(e).__name__)
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.
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"))
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.
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")
print("A") runs, then position 3 does not exist in a three-item list, which is an IndexError. print("B") is skipped, KeyError doesn't match, IndexError does, so D prints. The exception is handled, so the program carries on with 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.
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")
Lighting 3 lamps Ladder put away three is not a number Ladder put away
| Block | Runs when |
|---|---|
try | always first |
except | an exception of that kind happened in try |
else | try finished with no exception |
finally | always, 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:
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")
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:
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)
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 -5The 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.
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)
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.
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.
6 no miners
1def share(ore, miners):2 return ore // miners3 4print(share(20, 3))5print(share(20, 0))
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.
Total: 13 Skipped: 3
1readings = ["4", "x", "7", "", "2", "3.5"]2total = 03skipped = 04for r in readings:5 total += int(r)6print("Total:", total)7print("Skipped:", skipped)
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.
Loaded 40 Error: too heavy: 120 Loaded 90
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)
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 egives you the message.- Several
exceptblocks handle different kinds; parentheses handle several kinds the same way. elseruns when nothing went wrong;finallyruns every time.raise ValueError("...")raises an exception from your own code;class MyError(Exception): passmakes a kind of your own.