This lesson is being piloted (Beta version)

Errors and Exceptions

Overview

Teaching: 20 min
Exercises: 10 min
Questions
  • How do you handle errors in your Python code?

Objectives
  • Explain the difference between syntax errors and run-time exceptions.

  • Understand that Python has built-in exceptions, and where to find information on them.

  • Write code that handles exceptions using try-catch blocks.

  • Write code to raise an exception.

  • Understand the difference between the Look Before You Leap and Easier to Ask Forgiveness than Permission programming styles.

This episode contains Python features you may not have seen yet

This episode refers to some language features that you may not have seen before, as they are presented in later episodes.

Don’t worry about this, as the focus here is not on using these features. All you need to consider is that these operations can go wrong, and that there are different ways of dealing with the errors.

In all cases, the purpose of the code will be explained as we go.

Errors Happen

You have almost certainly encountered errors in your Python code. For beginners, the most common is the syntax error, which occurs when your code is not valid Python. For example:

>>> for i in range(10) print(i)
  File "<stdin>", line 1
    for i in range(10) print(i)
                           ^
SyntaxError: invalid syntax

Why does that example produce a syntax error?

The message “SyntaxError: invalid syntax” is correct, but not very helpful.

Note that the caret (^) indicates where Python detected a problem. What do you think the problem is?

Solution

The colon (:) that delimits the for loop from the loop body is missing. The reason that the print is marked with the caret is that any code following the for statement is invalid unless the statement is delimited by a colon (:). So print is the first invalid code even though the actual error occurs earlier.

The correct code would be:

for i in range(10): print(i)

Other types of errors cannot be detected when parsing your file. They are known as “Run-time Errors”.

Run the following snippet of Python code

numerator = 7
denominator = 0
result = numerator / denominator

What happens?

Solution

You get a ZeroDivisionError. This is the Python exception that indicates a run-time error caused by a division by zero. Note that the code is syntactically valid Python, so this is not a syntax error.

>>> numerator = 7
>>> denominator = 0
>>> result = numerator / denominator
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zero

The ZeroDivisionError is built-in to Python. The next exercise looks at some more built-in exceptions.

Exploring the Built-in Exceptions

Have a look at the documentation for built-in exceptions. See if you can find each of the following errors. If have encountered any other errors recently, see if you can find those as well.

  • ZeroDivisionError
  • FileNotFoundError
  • TypeError
  • MemoryError
  • IndexError
  • KeyError

Handle exceptions with try and catch

The standard pattern for handling exceptions is:

try:
    # Some code that might produce an error
    ...
catch KeyError:
    # Do something with the KeyError
    ...
catch NameError:
    ...
catch:
    # Catch any error possible
    ...
else:
    # Do something that only needs to execute if no exceptions were raised in
    # the `try` block.
    ...
finally:
    # This code runs at the end, regardless of whether there is an error or not
    # Typically used for clean-up actions
    ...

In addition to their type, exception objects contain other information that can help understand the error. This information can be seen by printing the exception. But to do this, the caught exception has to be assigned to a variable using the as keyword:

try:
    print(a)
catch NameError as e:
    print(e)

Raising Exceptions

The raise statement allows you to raise a specific exception from your code. It accepts a single argument - the exception to be raised:

>>> raise NameError('HiThere')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: HiThere

The other common use of raise is in a catch block. It is used when you want to do something with an exception but also allow the exception to propagate back to the outer code. For example:

try:
    function_that_uses_too_much_memory()
catch MemoryError:
    print("out of memory")
    raise  # causes the MemoryError to be raised again

A tale of two programming styles

You are writing some code that reads from an input file. Other programs, for reasons known only to them, occassionally create and destroy this file. Part of the design requirements are that your program should not crash if there are errors opening the file. In particular, if the file does not exist your program needs to print the message “I’m sorry, Dave. I’m afraid I can’t do that” (it seems that the designer is a 2001: A Space Odyssey fan).

Searching the Python library documentation you see that there is a function that lets you check for the existance of a file: os.path.isfile. It returns True if the file exists, otherwise it returns False. Armed with this knowledge, you write your code (assume that input_file_name is already assigned the correct value):

import os  # Makes the os.path.isfile function available to our code

if os.path.isfile(input_file_name):
    input_file = open(input_file_name)  # Nothing magic here, open() just opens a file
    process_file(input_file)
else:
    print("I'm sorry, Dave. I'm afraid I can't do that")

This code is an example of a defensive programming style often described by the catchy phrase “Look before you leap” (or LBYL). In LBYL programming, we check for possible problems before trying to execute the critical code. Examples include:

But there is a problem hiding in the previous solution. Since modern operating systems tend to do many things at the same time, it is possible for the file to be created or deleted in the time between the isfile check and attempting to open it. If the check passes but the file is deleted before opening it, open will raise the FileNotFoundError, leading to an unhandled exception and an incorrect program. This is an example of a race condition.

Now consider the following code:

try:
    input_file = open(input_file_name)
    process_file(input_file)
catch FileNotFoundError:
    print("I'm sorry, Dave. I'm afraid I can't do that")

This code does not check for the existance of the file before opening it. It just marches right in and tries to open the file. If a FileNotFoundError is raised, then this exception is caught and the required message is printed.

This is an example of a programming style called “It’s easier to ask forgiveness than permission” (or EAFP). While some describe this as the more Pythonic approach (and indeed you see this style a lot in Python), I think that both approaches can be valid in different circumstances.

When do you think one approach or the other will be more useful?

Some points to consider:

  • How bad is it if the error occurs?
  • How many circumstances do you need to check? Can you think of them all, or perhaps trust the library implementers to do so?
  • What is expected to occur most often: the error condition or the non-error case? - If the error case is rare, exceptions let you put it at the end of your code rather than the start.
  • Can the situation change between the error checks and the critical code?
  • Do the error checks duplicate code with the critical processing?

Is the Race Condition still present?

Is the race condition indicated in the LBYL example present in the EAFP solution? Discuss.

Key Points

  • Illegal language constructs are called syntax errors. They are detected by the Python parser.

  • Legal code can also produce errors during execution, known as run-time errors.

  • In Python, run-time errors raise exceptions.

  • Exceptions are handled with the try-catch language feature.

  • You can raise your own exceptions with the raise keyword.

  • Look Before You Leap (LBYL) is a defensive programming style where you check for problems before executing important code.

  • It’s Easier to Ask Forgiveness than Permission (EAFP) is a programming style where you wait for errors to occur and then handle them later.