Libraries and Modules in Python

Error detection and exceptions

Overview:

  • Teaching: 10 min
  • Exercises: 5 min

Questions

  • What are exceptions?
  • What are the benefits of using exceptions

Objectives

  • Understand that exceptions halt the operation of code
  • Know that we can use exceptions to highlight issues to our users
  • Know that exceptions help us provide useful information for our users

We all make mistakes

Mistakes and errors happen in computer programs as much as in real life. Like life, how you handle an error in your program shows your level of professionalism, and gives others evidence that they can trust that you have written a program that will work well.

In the last section we indicated errors in the Person.setHeight function by printing a message to the screen and returning False to indicate that the call to setHeight had failed.

In [1]:
class Person:
    """Class that holds a person's height"""
    def __init__(self):
        """Construct a person who has zero height"""
        self._height = 0
    
    def setHeight(self, height):
        """Set the person's height to 'height', returning whether or 
           not the height was set successfully
        """
        if height < 0 or height > 300:
            print("This is an invalid height! %s" % height)
            return False
        else:
            self._height = height
            return True
        
    def getHeight(self):
        """Return the person's height"""
        return self._height
In [2]:
p = Person()
In [3]:
p.setHeight(-20)
This is an invalid height! -20
Out[3]:
False

Why is this a problem?

This is not a good way of indicating an error. The issues with this are;

  • How does the person calling getHeight know to check whether the call returns True or False
  • What if we wanted to return something else? Should we return the error state and the value we want together?
  • If the error state is not checked, and nobody reads the error message printed to the screen, then the program is broken, as the person has been created with a height of 0.

The solution is to send something to the programmer that they cannot ignore, which indicates that there is an error. That something is called an "exception".

Take a look at this simple code that sets the height...

In [4]:
def setHeight(height):
    if height < 0 or height > 300:
        raise ValueError("Invalid height: %s. This should be between 0 and 300" % height)
        
    print("Height is set to %s" % height)
In [5]:
setHeight(-5)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-5-c12728f7251e> in <module>
----> 1 setHeight(-5)

<ipython-input-4-ab06ec20248c> in setHeight(height)
      1 def setHeight(height):
      2     if height < 0 or height > 300:
----> 3         raise ValueError("Invalid height: %s. This should be between 0 and 300" % height)
      4 
      5     print("Height is set to %s" % height)

ValueError: Invalid height: -5. This should be between 0 and 300

Inform and assist your users

When we try to use an invalid value for the height, we raise (or throw) a ValueError exception. This stops the function from continuing, and gives us a very helpful print out of what went wrong, and where.

ValueError is just a class. The name of the class provides us with useful information (there was an error with a value in the program). You choose what error you want to raise. Python provides a set of usefully named error classes that you can use:

  • IOError : Error raised when you have a problem with IO, e.g. opening or closing files
  • ZeroDivisionError : Error raised when you divide by zero
  • TypeError : Error raised when you are using the wrong type, e.g. maybe setting the height to a string
  • IndexError : Error raised when you are using an invalid index to access a list or other similar container
  • KeyError : Error raised when you are using an invalid key to access a dictionary or other similar container

A full list of standard Python exceptions is available here.

You are free to raise any exception class you want. It is your job as a programmer to choose the one that is most sensible, e.g.

In [6]:
def setHeight(height):
    if height < 0 or height > 300:
        raise ZeroDivisionError("Invalid height: %s. This should be between 0 and 300" % height)
        
    print("Height is set to %s" % height)
In [7]:
setHeight(400)
---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-7-0fd638ddbef1> in <module>
----> 1 setHeight(400)

<ipython-input-6-0689287eab0a> in setHeight(height)
      1 def setHeight(height):
      2     if height < 0 or height > 300:
----> 3         raise ZeroDivisionError("Invalid height: %s. This should be between 0 and 300" % height)
      4 
      5     print("Height is set to %s" % height)

ZeroDivisionError: Invalid height: 400. This should be between 0 and 300

Using a ZeroDivisionError is a bad choice, as the error has nothing to do with division by zero. A ValueError is the right choice as the error relates to an invalid value passed to the function.

You are free to create your own exception classes.

In [8]:
class InvalidHeightError(Exception):
    pass
In [9]:
def setHeight(height):
    if height < 0 or height > 300:
        raise InvalidHeightError("Invalid height: %s. This should be between 0 and 300" % height)
        
    print("Height is set to %s" % height)
In [10]:
setHeight(-10)
---------------------------------------------------------------------------
InvalidHeightError                        Traceback (most recent call last)
<ipython-input-10-ad154962daaa> in <module>
----> 1 setHeight(-10)

<ipython-input-9-38ff29b41fc5> in setHeight(height)
      1 def setHeight(height):
      2     if height < 0 or height > 300:
----> 3         raise InvalidHeightError("Invalid height: %s. This should be between 0 and 300" % height)
      4 
      5     print("Height is set to %s" % height)

InvalidHeightError: Invalid height: -10. This should be between 0 and 300

Your own exception classes must be declared as derived from type Exception, hence why you have to write class InvalidHeightError(Exception):. As the class doesn't need to do anything else, you can use pass to say that nothing else needs to be added. Note that you can call your error class anything you want. By convention, it is good to end the class name with Error so that other programmers know what it is for.

Exercise

Here is an extended version of the Person code from the previous lesson.

In [11]:
class Person:
    """Class that holds a person's height"""
    def __init__(self, height=0, weight=0):
        """Construct a person with the specified name, height and weight"""
        self.setHeight(height)
        self.setWeight(weight)
    
    def setHeight(self, height):
        """Set the person's height in meters"""
        self._height = height
    
    def setWeight(self, weight):
        """Set the person's weight in kilograms"""
        self._weight = weight
        
    def getHeight(self):
        """Return the person's height in meters"""
        return self._height
    
    def getWeight(self):
        """Return the person's weight in kilograms"""
        return self._weight
    
    def bmi(self):
        """Return the person's body mass index (bmi)"""
        return self.getWeight() / self.getHeight()**2

1

Edit the Person class to ensure that the .setWeight function only accepts valid weights. A valid weight is any number that is between 0 and 500 kilograms. You should raise a ValueError if the weight is outside this range. For the moment, do not worry about the user supplying a non-numeric weight.

Also edit the above copy of Person to ensure that the .setHeight function only accepts valid heights. A valid height is any number that is between 0 and 2.5 meters. You should raise a ValueError if the height is outside this range. For the moment, do not worry about the user supplying a non-numeric height.

Check that a ValueError exception is correctly raised if invalid heights or weights are supplied. Also check that the ValueError exception is not raised if a valid height and weight are supplied.

Solution

2

If you run the following code;

p = Person()
p.bmi()

it will raise a DivideByZero exception. This is because the calculation involves dividing by the height squared, which is zero in a default-constructed Person. While an exception has been raised, it is not very intuitive for another programmer to debug. A solution is to create your own named exception that provides more information.

Create a new exception called NullPersonError, and edit the .bmi() function so that this exception is raised if it is called on a Person whose height or weight is zero.

Check that the NullPersonError exception is raised if .bmi() is called on a default-constructed Person. Check that this exception is not raised if .bmi() is called on a properly constructed Person.

Solution

Key Points:

  • Exceptions allow you to halt code execution and inform users what has gone wrong
  • Use standard exceptions or define your own to help users fix their mistakes