Exceptions

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

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-d79fd1d8e207> in <module>()
----> 1 setHeight(-5)

<ipython-input-4-1e09cd9970ae> 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

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-2e20bc96d034> in <module>()
----> 1 setHeight(400)

<ipython-input-6-e27119f86062> 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-94e46abff43e> in <module>()
----> 1 setHeight(-10)

<ipython-input-9-ced177df8f8e> 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 copy of the Person code from above.

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"""
        if height < 0 or height > 2.5:
            raise ValueError("Invalid height: %s. This shoud be between 0 and 2.5 meters" % height)
        self._height = height
    
    def setWeight(self, weight):
        """Set the person's weight in kilograms"""
        if weight < 0 or weight > 500:
            raise ValueError("Invalid weight: %s. This should be between 0 and 500 kilograms" % weight)
        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

Exercise 1

Edit the above copy of Person 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.

In [12]:
p = Person(height=2.8, weight=500)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-12-a9b5ab422dd0> in <module>()
----> 1 p = Person(height=2.8, weight=500)

<ipython-input-11-b83565801c3e> in __init__(self, height, weight)
      3     def __init__(self, height=0, weight=0):
      4         """Construct a person with the specified name, height and weight"""
----> 5         self.setHeight(height)
      6         self.setWeight(weight)
      7 

<ipython-input-11-b83565801c3e> in setHeight(self, height)
      9         """Set the person's height in meters"""
     10         if height < 0 or height > 2.5:
---> 11             raise ValueError("Invalid height: %s. This shoud be between 0 and 2.5 meters" % height)
     12         self._height = height
     13 

ValueError: Invalid height: 2.8. This shoud be between 0 and 2.5 meters
In [13]:
p = Person(height=1.8, weight=501)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-13-431e2b96b8b9> in <module>()
----> 1 p = Person(height=1.8, weight=501)

<ipython-input-11-b83565801c3e> in __init__(self, height, weight)
      4         """Construct a person with the specified name, height and weight"""
      5         self.setHeight(height)
----> 6         self.setWeight(weight)
      7 
      8     def setHeight(self, height):

<ipython-input-11-b83565801c3e> in setWeight(self, weight)
     15         """Set the person's weight in kilograms"""
     16         if weight < 0 or weight > 500:
---> 17             raise ValueError("Invalid weight: %s. This should be between 0 and 500 kilograms" % weight)
     18         self._weight = weight
     19 

ValueError: Invalid weight: 501. This should be between 0 and 500 kilograms
In [14]:
p = Person(height=1.8, weight=150)

Exercise 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.

In [15]:
class NullPersonError(Exception):
    pass
In [16]:
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"""
        if height < 0 or height > 2.5:
            raise ValueError("Invalid height: %s. This shoud be between 0 and 2.5 meters" % height)
        self._height = height
    
    def setWeight(self, weight):
        """Set the person's weight in kilograms"""
        if weight < 0 or weight > 500:
            raise ValueError("Invalid weight: %s. This should be between 0 and 500 kilograms" % weight)
        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)"""
        if (self.getHeight() == 0 or self.getWeight() == 0):
            raise NullPersonError("Cannot calculate the BMI of a person with zero "
                                  "height or weight (%s,%s)" % (self.getHeight(),self.getWeight()))
            
        return self.getWeight() / self.getHeight()**2
In [17]:
p = Person()
In [18]:
p.bmi()
---------------------------------------------------------------------------
NullPersonError                           Traceback (most recent call last)
<ipython-input-18-ac90f82815e5> in <module>()
----> 1 p.bmi()

<ipython-input-16-061742ffc8d5> in bmi(self)
     30         if (self.getHeight() == 0 or self.getWeight() == 0):
     31             raise NullPersonError("Cannot calculate the BMI of a person with zero "
---> 32                                   "height or weight (%s,%s)" % (self.getHeight(),self.getWeight()))
     33 
     34         return self.getWeight() / self.getHeight()**2

NullPersonError: Cannot calculate the BMI of a person with zero height or weight (0,0)
In [19]:
p = Person(height=1.8, weight=77.5)
In [20]:
p.bmi()
Out[20]:
23.919753086419753
In [ ]: