Exception handling
try, except, else and finally, raising your own exceptions, and keeping the motors safe when something goes wrong.
Do this lesson in the simulatorAt GCSE you met runtime errors (F1.4) and validated input before using it (F6.1). A runtime error in Python is an exception: an object that describes what went wrong, raised at the moment it happens. If nothing handles it, the program stops and prints a message. On a laptop that is annoying; on a robot it can mean the motors are left running while the program that should stop them has ended. Exception handling lets a program catch an exception, deal with it, and carry on or shut down safely.
An unhandled exception
readings = []
average = sum(readings) / len(readings)
print("average", average)
The program stops on the second line with ZeroDivisionError: division by zero, and the print never runs. The message names the type of exception and gives a description.
Some exception types you will meet:
| Exception | Raised when | Example |
|---|---|---|
ValueError |
A value is the right type but unsuitable | int("twenty") |
TypeError |
An operation is given the wrong type | "cm" + 5 |
ZeroDivisionError |
Dividing by zero | 5 / 0 |
IndexError |
An index is past the end of a list | [1, 2][5] |
KeyError |
A dictionary has no such key | {"cm": 5}["heading"] |
FileNotFoundError |
Opening a file that does not exist | open("missing.csv") |
try and except
Code that might raise an exception goes in a try block. If an exception is raised there, Python jumps straight to a matching except block; the rest of the try block is skipped.
def average(readings):
try:
return sum(readings) / len(readings)
except ZeroDivisionError:
return 0.0
print(average([31.0, 51.0, 15.0]))
print(average([]))
Name the exception you expect. A bare except: catches everything, including mistakes you did not expect, such as a misspelt variable name, and hides them. Catch what you can deal with and let the rest stop the program, where you will see them.
else, finally and as
A full handler has four parts:
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
for text in ["15", "fifteen"]:
try:
cm = int(text) # might raise ValueError
except ValueError as error:
print("could not use", text, ":", error)
else:
forward(50, distance=cm) # only when the try block raised nothing
print("drove", cm)
finally:
stop() # always, whatever happened
print("motors stopped")
except ValueError as errornames the exception object, so its message can be printed or logged.elseruns only if thetryblock finished without an exception. Keeping the driving out of thetryblock means aValueErrorfrom somewhere else cannot be mistaken for bad input.finallyruns every time: after success, after a handled exception, and even when an exception is not handled and the program is about to stop. It is the place for clean-up that must never be skipped: stopping motors, closing files, releasing a connection.
Raising your own
A subroutine can raise an exception to signal that it cannot do its job. The caller decides what to do about it:
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
def safe_forward(cm):
if cm < 0 or cm > 50:
raise ValueError("distance must be 0 to 50 cm, not " + str(cm))
if distance() - cm < 10:
raise RuntimeError("that would hit the wall")
forward(50, distance=cm)
for cm in [20, 80, 45]:
try:
safe_forward(cm)
print("drove", cm)
except ValueError as error:
print("bad request:", error)
except RuntimeError as error:
print("refused:", error)
Separate except blocks deal with different types in different ways. An exception that is not caught in the subroutine where it is raised passes back to the code that called it, and then to the code that called that, until something catches it or the program stops. Module A2 shows the call stack this travels back through.
You can also define an exception type of your own, as a class that inherits from Exception. The syntax makes sense after lesson A1.8, and the project uses it:
class Blocked(Exception):
pass
try:
raise Blocked("wall 8 cm ahead")
except Blocked as error:
print("caught:", error)
Exceptions or checks?
There are two ways to deal with bad data. Check first with an if before acting, as the validation in F6.1 did. Or try it and handle the exception. Checking first is clearer for expected situations such as a number out of range. Exceptions are better when the check would repeat the work, as it would for deciding whether a string is a valid number, which int already does; when the problem is detected deep inside a subroutine and has to be reported to code several calls away; and for failures outside the program's control, such as a missing file or a lost radio link.
Task: a distance that cannot crash the program
Write a function read_distance(text) that takes a string text and returns it as an integer. It must raise a ValueError in two cases: when text is not a whole number (let int raise it) and when the number is below 1 or above 100 (raise it yourself with raise ValueError).
In the main program, keep asking Distance (1 to 100)? with input until read_distance succeeds. Each time it raises ValueError, print rejected <text>, where <text> is exactly what was typed. The task types twenty, 250, -5 and then 25. When you have a valid distance, drive forward that many cm inside a try block whose finally block calls stop() and prints motors stopped.
# the two lines every program starts with: the commands, then the robot
from bugbot import *
connect()
cm = int(input("Distance (1 to 100)? "))
forward(50, distance=cm)
print("motors stopped")
Challenges
- Add a second
exceptforTypeError. Canread_distanceever raise one, wheninputalways returns a string? - Give up after three rejected answers by raising an exception of your own, and catch it in the main program.
- Put
print(1 / 0)inside thetryblock that drives. Doesmotors stoppedstill print? Does the program carry on afterwards?