Libraries and Modules in Python

Error Handling

Overview:

  • Teaching: 10 min
  • Exercises: 5 min

Questions

  • How can I handle exceptions?
  • What happens if different errors could be causing the exception?
  • How can I use exceptoins to improve the usability of my code?

Objectives

  • Understand how to use try statements to handle exceptions
  • Understand that careful exception handling can help make your code more usable

Using exceptions to handle errors

Exceptions are useful for more than just signalling errors. They can also be used to help you handle the error, and potentially even fix the problem (true self-healing program!).

Consider this cut down version of the .setHeight function from the last session...

In [1]:
def setHeight(height):
    if height < 0 or height > 2.5:
        raise ValueError("Invalid height: %s. This should be between 0 and 2.5 m" % height)
    print("setting the height to %s" % height)

The code currently correctly detects if the user supplies a height that is below 0 or above 2.5. However, what about when the user tries to set the height to something that is not a number?

In [2]:
setHeight("cat")
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-2-e7c52f2ea810> in <module>
----> 1 setHeight("cat")

<ipython-input-1-ff12b3707c8f> in setHeight(height)
      1 def setHeight(height):
----> 2     if height < 0 or height > 2.5:
      3         raise ValueError("Invalid height: %s. This should be between 0 and 2.5 m" % height)
      4     print("setting the height to %s" % height)

TypeError: '<' not supported between instances of 'str' and 'int'

We get a weird error message that says we have a TypeError, as you cannot order a string and an integer.

One way to address this is to ask that height is converted to a float, using height = float(height)

In [3]:
def setHeight(height):
    height = float(height)
    
    if height < 0 or height > 2.5:
        raise ValueError("Invalid height: %s. This should be between 0 and 2.5 m" % height)
    print("setting the height to %s" % height)

However, this hasn't made the error any easier to understand, as we now get a ValueError raised...

In [4]:
setHeight("cat")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-4-e7c52f2ea810> in <module>
----> 1 setHeight("cat")

<ipython-input-3-df7b2194ecbb> in setHeight(height)
      1 def setHeight(height):
----> 2     height = float(height)
      3 
      4     if height < 0 or height > 2.5:
      5         raise ValueError("Invalid height: %s. This should be between 0 and 2.5 m" % height)

ValueError: could not convert string to float: 'cat'

The solution is for us to handle the exception, using a try...except block

In [5]:
def setHeight(height):
    try:
        height = float(height)
    except:
        raise TypeError("Invalid height: '%s'. You can only set the height to a numeric value" % height)
    
    if height < 0 or height > 2.5:
        raise ValueError("Invalid height: %s. This should be between 0 and 2.5 m" % height)
    print("setting the height to %s" % height)
In [6]:
setHeight("cat")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-5-ca55e2a1740a> in setHeight(height)
      2     try:
----> 3         height = float(height)
      4     except:

ValueError: could not convert string to float: 'cat'

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
<ipython-input-6-e7c52f2ea810> in <module>
----> 1 setHeight("cat")

<ipython-input-5-ca55e2a1740a> in setHeight(height)
      3         height = float(height)
      4     except:
----> 5         raise TypeError("Invalid height: '%s'. You can only set the height to a numeric value" % height)
      6 
      7     if height < 0 or height > 2.5:

TypeError: Invalid height: 'cat'. You can only set the height to a numeric value

What's happened here? The try: line starts a try-block. The code that is in the try-block is run. If any of this code raises an exception, then execution stops in the try-block, and switches instead to the code in the except-block (everything within the except: block). In our case, float(height) raised an exception, so execution jumped to the except-block, in which we ran the raise TypeError(...) code.

Now the error is much more informative, allowing the user to better understand what has gone wrong. However, exception handling can do more than this. It can allow you to fix the problem. Consider this example...

In [7]:
setHeight("1.8 m")
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
<ipython-input-5-ca55e2a1740a> in setHeight(height)
      2     try:
----> 3         height = float(height)
      4     except:

ValueError: could not convert string to float: '1.8 m'

During handling of the above exception, another exception occurred:

TypeError                                 Traceback (most recent call last)
<ipython-input-7-b55c104fd729> in <module>
----> 1 setHeight("1.8 m")

<ipython-input-5-ca55e2a1740a> in setHeight(height)
      3         height = float(height)
      4     except:
----> 5         raise TypeError("Invalid height: '%s'. You can only set the height to a numeric value" % height)
      6 
      7     if height < 0 or height > 2.5:

TypeError: Invalid height: '1.8 m'. You can only set the height to a numeric value

We as humans can see that this could be an acceptable input. However, the computer needs help to understand. We can add code to the except-block that can try to resolve the problem. For example, imagine we had a function that could interpret heights from strings...

In [8]:
def string_to_height(height):
    """This function tries to interpret the passed argument as a height 
       in meters. The format should be 'X m', 'X meter' or 'X meters',
       where 'X' is a number
    """
    # convert height to a string - this always works
    height = str(height)
        
    words = height.split(" ")
            
    if len(words) == 2:
        if words[1] == "m" or words[1] == "meter" or words[1] == "meters":
            try:
                return float(words[0])
            except:
                pass
    
    # Getting here means that we haven't been able to extract a valid height
    raise TypeError("Cannot extract a valid height from '%s'" % height)

We can now call this function from within the except-block of setHeight

In [9]:
def setHeight(height):
    try:
        height = float(height)
    except:
        height = string_to_height(height)
    
    if height < 0 or height > 2.5:
        raise ValueError("Invalid height: %s. This should be between 0 and 2.5 m" % height)
    print("setting the height to %s" % height)
In [10]:
setHeight("1.8 m")
setting the height to 1.8

Exercise

1

Here is a copy of the Person class from the last session. Edit the setHeight function so that it uses exception handling and the string_to_height function to correctly interpret heights such as "1.8 m", and so that it gives a useful error message if it is given something weird. Check that the function correctly responds to a range of valid and invalid inputs.

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

2

Create a string_to_weight function that interprets weights in kilograms (e.g. "5 kg", "5 kilos" or "5 kilograms"). Now edit the Person.setWeight function so that it uses exception handling and string_to_weight to to correctly interpret weights such as 35.5 kg and gives a useful error message if it is given something weird. Check that your function responds correctly to a range of valid and invalid inputs.

Solution

Key Points:

  • We can use try, except blocks to handle exceptions
  • We can handle exceptions intelligently, and carefully to deal with common issues/mistakes/usage.