Exceptions


Martin McBride, 2021-04-21
Tags exceptions errors try except raise finally else
Categories python language beginning python

In this section we will look at exceptions:

  • Program errors.
  • What are exceptions?
  • Exceptions types.
  • Catching exceptions.
  • Using else with exceptions.
  • Using finally with exceptions.
  • Throwing exceptions.

Program errors

Computer programs sometimes go wrong. There are three broad types of error you will encounter when you are writing code:

  • Syntax errors.
  • Logical errors.
  • Runtime errors.

Syntax errors occur when the code you type in isn't valid. This is often due to typing errors or misunderstanding Python syntax. For example:

s = [1, 2, 3           # Missing end bracket
1a = 3                 # Variable name can't start with a number
foor i in range(10):   # Misspelling for
    do_something()
   do_something_else() # Wrong indentation

This code is full of mistakes, and Python simply won't run it until you fix things. These errors are usually the easiest to find and fix because Python highlights them.

Logical errors are where you have typed invalid Python code, but the code you typed in doesn't do what you thought it would. For example:

x = 10
if x <= 10:
    print('x is less than 10')

The problem here is that the code is valid but logically incorrect. The programmer wanted the message to only be displayed is x was less than ten, but the code actually checks if x is less than or equal to ten. These types of error can be more difficult to spot because they often only happen in specific circumstances (the code above works for any number except 10). You can only really eliminate these bugs by testing your code thoroughly. But the good news is, once you have spotted the bug it happens every time (the code will always go wrong for the value 10), which makes it easier to find.

Runtime errors are things that go wrong because of external factors. For example:

  • If your program saves a file to disk, it will fail if the disk is full.
  • If your program reads data from the internet, it will fail if the network is disconnected.
  • If your program asks the user to type in a number, but they type in "hello" instead.

You can write extra code to check for these things, but you can't catch everything. For example, your program might check that the network is connected before it tries to read some data, but what happens if someone unplugs the cable while the data. This is where exceptions come in.

What are exceptions

Here is an example of exceptions in action:

age = int(input('How old are you?'))
print('You are', age, 'years old')

This code works fine provided the user types in a number. But if they type in something else, such as "hello", the program terminates with a console message:

Traceback (most recent call last):
  File "test.py", line 1, in <module>
    age = int(input('How old are you?'))
ValueError: invalid literal for int() with base 10: 'hello'

The problem here is that the int function is being handed a value 'hello'. Since int cannot convert this string into a number, it can't return a sensible value. So the int function doesn't return in the normal way at all. Instead, it raises an exception. This exception causes Python to abandon the normal running of the program, and jump right out of the program back to the console.

This type of behaviour is called raising an exception because it only happens in exceptional circumstances (in this case, when the user provides bad input). It is also sometimes called throwing an exception, which means the same thing.

When an exception is thrown, Python also provides an Exception object that gives more information about the causes of the error.

The console detects that the program has raised an exception, and displays the message above. The information in the error message comes from the Exception object.

One final aspect of exceptions that is incredibly useful is that you can catch and exception in your own code. This allows your code to check the Exception object, deal with the problem, and carry on running. Here is an example:

try:
    age = int(input('How old are you?'))
    print('You are', age, 'years old')
except:
    print('Invalid age value')
    age = None

We will explain this in more detail below, but basically, we have placed our main code in a try block, and our error handling code in an except block (the syntax is similar to if and else). The way this works is that if an exception is raised in the try block, it is caught and causes the except block to run, which in this case simply prints a message and sets the age to None. The program then continues as before.

In no exceptions occur within the try block, the except block is completely skipped.

Exception types

There are dozens of built-in exceptions, we will look at some of the common ones here.

ImportError

This type of exception is thrown if your code tries to import something that doesn't exist:

from math import sqrt # This is ok, math has a sqrt function
from math import xyz  # ImportError, math has no xyz function

IndexError

This type of exception is thrown if your code tries to access an out of range list element:

k = [1, 2, 3, 4] # k has 4 elements
k[1]             # This is ok, element 1 exists
k[6]             # IndexError, there is no element 6, the list is only 4 long

TypeError

This type of exception is thrown if as operation fails because of the type of data:

len('abc')   # This is ok, the length of the string is 3.
len(10)      # TypeError, you can't find the length of an integer

ValueError

This type of exception is thrown if an operation fails because of an invalid value, for example:

a = int('1')      # This is fine, '1' can be coverted to and integer.
a = int('hello')  # ValueError, 'hello' is not a number

ZeroDivisionError

This type of exception is thrown if you try to divide a number by zero, for example:

a = 1/0   # ZeroDivisionError

Catching exceptions

We have already seen how to catch an exception. We will now look at this is a bit more detail.

Here is a program where we ask the user for a number, and display the corresponding day of the week:

days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
day_no = int(input('Enter a day number 0-6 '))
day = days[day_no]
print('The day is', day)

Clearly we have the same problem as with our earlier age program - the user could enter an invalid string. We can use the same solution:

try:
    days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
    day_no = int(input('Enter a day number 0-6 '))
    day = days[day_no]
except:
    print('Invalid day number')
    day = None

if day:
    print('The day is', day)

Only catch exceptions you can handle

The code above will catch any exception that gets thrown when the code in the try block runs.

Generally, it is better to only catch the exceptions that you are intending to handle. If a completely different exception occurs, that our software knows nothing about, it is usually best to let it go. Some other part of the system might be set up to handle that exception properly, it is best to allow that to happen.

We can do that by specifically catching a particular exception, for example we would expect a ValueError if the user typed in an invalid string:

try:
    days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
    day_no = int(input('Enter a day number 0-6 '))
    day = days[day_no]
except ValueError:
    print('Invalid day number')
    day = None

if day:
    print('The day is', day)

So now our code will handle a ValueError and carry on working.

But if some other type of exception occurs, for example, a network error, our code can't do anything about it. We ignore any unwanted exceptions and hopefully the code that called our code will handle it.

However, we do need to make sure we handle all the exceptions we can. In the code above, what if the user typed in '8'? The int function would covert the string into an integer, but days[day_no] would throw an IndexError because there are only 7 days in the days list. We actually need to check for both types of error. We can do it like this:

try:
    days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
    day_no = int(input('Enter a day number 0-6 '))
    day = days[day_no]
except (ValueError, IndexError):
    print('Invalid day number')
    day = None

if day:
    print('The day is', day)

In this case, we are using a tuple of exception types (ValueError, IndexError) and the except clause applies to any type in that tuple. You need to put brackets around the tuple.

Alternatively, we can have different except clauses for each type, like this:

try:
    days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
    day_no = int(input('Enter a day number 0-6 '))
    day = days[day_no]
except ValueError:
    print('Invalid number string')
    day = None
except IndexError:
    print('Day number must be 0 to 6')
    day = None

if day:
    print('The day is', day)

This allows us to handle the two exceptions differently.

Accessing the exception message

Exceptions often contain additional information about what went wrong. You can access the exception within the except block like this:

try:
    days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
    day_no = int(input('Enter a day number 0-6 '))
    day = days[day_no]
except ValueError as e:
    print(e)
    day = None
except IndexError as e:
    print(e)
    day = None

if day:
    print('The day is', day)

Using as makes the exception object available to our code in the variable e.

This time, instead of printing a custom message, we print the exception object itself.

You will sometimes want to do both - you can display a helpful, custom message to explain what has gone wrong to the user, and also display the error content to help debug the problem.

Using else with exceptions

We can add an else clause to the end of our try statement, like this:

try:
    days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
    day_no = int(input('Enter a day number 0-6 '))
    day = days[day_no]
except ValueError:
    print('Invalid number string')
except IndexError:
    print('Day number must be 0 to 6')
else:
    print('The day is', day)

The else clause only gets called if no exception occurs. In this case, we are using it to print the result.

In all cases, the code executes exactly one of the clauses - either a one except clause or the else clause.

Using finally with exceptions

If you add a finally block to a try statement, it will always get executed at the end, no matter what:

try:
    days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']
    day_no = int(input('Enter a day number 0-6 '))
    day = days[day_no]
except ValueError:
    print('Invalid number string')
except IndexError:
    print('Day number must be 0 to 6')
else:
    print('The day is', day)
finally:
    print('All done!')

The finally clause always runs:

  • If an exception occurs and is caught, the matching except clause runs, followed by the finally clause.
  • If no exception occurs, the else clause runs (if there is one), followed by the finally clause.
  • Even if an unexpected exception occurs, there will be no matching except clause, but the finally clause still runs.

Although we have used finally here to print a message, it is usually used for "tidying up" type task, such as closing any files that the program might have been using. This ensures that the program always gets a chance to do what it needs to do, even if an error occurs.

Throwing exceptions

Consider this code:

def divide(a, b):
    return a/b

print(divide(3, 2))
print(divide(3, 0))

In the first print statement, divide(3, 2) returns 1.5, which is printed. In the second print statement, divide(3, 0) throws a divide by zero error.

Our code could check the value of b for zero, then raise a different exception that provides more specific information about he problem:

def divide(a, b):
    if b == 0:
        raise ValueError('b cannot be zero')
    return a/b

print(divide(3, 2))
print(divide(3, 0))

This time if b is zero, the code raises a ValueError with a message saying that b cannot be zero.

You can also catch and re-raise exceptions:

def divide(a, b):
    try:
        return a/b
    except ZeroDivisionError:
        raise ValueError('b cannot be zero')

print(divide(3, 2))
print(divide(3, 0))

In this case, rather than checking for b being zero, we just calculate a/b. If b is zero, this will throw a ZeroDivisionError exception. We then catch this exception and throw a ValueError exception. This allows us to swap one exception for another.

If you found this article useful, you might be interested in the book Python Quick Start or other books by the same author.

Prev

Popular tags

2d arrays abstract data type alignment and animation arc array arrays behavioural pattern bezier curve built-in function callable object circle classes close closure cmyk colour comparison operator comprehension context context manager conversion creational pattern data types design pattern device space dictionary drawing duck typing efficiency else encryption enumerate fill filter font font style for loop function function composition function plot functools game development generativepy tutorial generator geometry gif gradient greyscale higher order function hsl html image image processing imagesurface immutable object index inner function input installing iter iterable iterator itertools l system lambda function len line linspace list list comprehension logical operator lru_cache magic method mandelbrot mandelbrot set map monad mutability named parameter numeric python numpy object open operator optional parameter or partial application path polygon positional parameter print pure function pycairo python standard library radial gradient range recipes rectangle recursion reduce rgb rotation scaling sector segment sequence singleton slice slicing sound spirograph sprite square str strategy stream string stroke structural pattern subpath symmetric encryption template text text metrics tinkerbell fractal transform translation transparency tuple turtle unpacking user space vectorisation webserver website while loop zip