Exception handling

try, except, else and finally, raising your own exceptions, and keeping the motors safe when something goes wrong.

A1.3Programming techniques and object-oriented programmingA level20 min

Do this lesson in the simulator

At 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)

Run this in the simulator

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([]))

Run this in the simulator

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")

Run this in the simulator

  • except ValueError as error names the exception object, so its message can be printed or logged.
  • else runs only if the try block finished without an exception. Keeping the driving out of the try block means a ValueError from somewhere else cannot be mistaken for bad input.
  • finally runs 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)

Run this in the simulator

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)

Run this in the simulator

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

  1. Add a second except for TypeError. Can read_distance ever raise one, when input always returns a string?
  2. Give up after three rejected answers by raising an exception of your own, and catch it in the main program.
  3. Put print(1 / 0) inside the try block that drives. Does motors stopped still print? Does the program carry on afterwards?