Error handling and testing solutions

Introduction

In this notebook we will try to understand what our program should do when it encounters unforeseen situations, and how to test the code we write.

For some strange reason, many people believe that computer programs do not need much error handling nor testing. Just to make a simple comparison, would you ever drive a car that did not undergo scrupolous checks? We wouldn’t.

What to do

  1. unzip exercises in a folder, you should get something like this:

functions
   fun1-intro.ipynb
   fun1-intro-sol.ipynb
   fun2-errors-and-testing.ipynb
   fun2-errors-and-testing-sol.ipynb
   fun3-strings.ipynb
   fun3-strings-sol.ipynb
   fun4-lists.ipynb
   fun4-lists-sol.ipynb
   fun5-tuples.ipynb
   fun5-tuples-sol.ipynb
   fun6-sets.ipynb
   fun6-sets-sol.ipynb
   fun7-dictionaries.ipynb
   fun7-dictionaries-sol.ipynb
   fun8-chal.ipynb
   jupman.py

WARNING: to correctly visualize the notebook, it MUST be in an unzipped folder !

  1. open Jupyter Notebook from that folder. Two things should open, first a console and then browser. The browser should show a file list: navigate the list and open the notebook functions/fun2-errors-and-testing.ipynb

  2. Go on reading that notebook, and follow instuctions inside. Sometimes you will find cells marked with Exercise which will ask you to write Python commands in the following cells.

Shortcut keys:

  • to execute Python code inside a Jupyter cell, press Control + Enter

  • to execute Python code inside a Jupyter cell AND select next cell, press Shift + Enter

  • to execute Python code inside a Jupyter cell AND a create a new cell aftwerwards, press Alt + Enter

  • If the notebooks look stuck, try to select Kernel -> Restart

Unforeseen situations

It is evening, there is to party for a birthday and they asked you to make a pie. You need the following steps:

  1. take milk

  2. take sugar

  3. take flour

  4. mix

  5. heat in the oven

You take the milk, the sugar, but then you discover there is no flour. It is evening, and there aren’t open shops. Obviously, it makes no sense to proceed to point 4 with the mixture, and you have to give up on the pie, telling the guest of honor the problem. You can only hope she/he decides for some alternative.

Translating everything in Python terms, we can ask ourselves if during the function execution, when we find an unforeseen situation, is it possible to:

  1. interrupt the execution flow of the program

  2. signal to whoever called the function that a problem has occurred

  3. allow to manage the problem to whoever called the function

The answer is yes, you can do it with the mechanism of exceptions (Exception)

make_problematic_pie

Let’s see how we can represent the above problem in Python. A basic version might be the following:

[2]:
def make_problematic_pie(milk, sugar, flour):
    """ Suppose you need 1.3 kg for the milk, 0.2kg for the sugar and 1.0kg for the flour

        - takes as parameters the quantities we have in the sideboard
    """

    if milk > 1.3:
        print("take milk")
    else:
        print("Don't have enough milk !")

    if sugar > 0.2:
        print("take sugar")
    else:
        print("Don't have enough sugar!")

    if flour > 1.0:
        print("take flour")
    else:
        print("Don't have enough flour !")

    print("Mix")
    print("Heat")
    print("I made the pie!")


make_problematic_pie(5,1,0.3)  # not enough flour ...

print("Party")
take milk
take sugar
Don't have enough flour !
Mix
Heat
I made the pie!
Party

QUESTION: this above version has a serious problem. Can you spot it ??

Show answer

Check with the return

EXERCISE: We could correct the problems of the above pie by adding return commands. Implement the following function.

WARNING: DO NOT move the print("Party") inside the function

The exercise goal is keeping it outside, so to use the value returned by make_pie for deciding whether to party or not.

If you have any doubts on functions with return values, check Chapter 6 of Think Python

Show solution
[3]:
def make_pie(milk, sugar, flour):
    """  - suppose we need 1.3 kg for milk, 0.2kg for sugar and 1.0kg for flour

         - takes as parameters the quantities we have in the sideboard
         IMPROVE WITH return COMMAND: RETURN True if the pie is doable,
                                             False otherwise

         *OUTSIDE* USE THE VALUE RETURNED TO PARTY OR NOT

    """
    # implement here the function



# now write here the function call, make_pie(5,1,0.3)
# using the result to declare whether it is possible or not to party :-(


take milk
take sugar
Don't have enough flour !
No party !

Exceptions

Real Python - Python Exceptions: an Introduction

Using return we improved the previous function, but remains a problem: the responsability to understand whether or not the pie is properly made is given to the caller of the function, who has to take the returned value and decide upon that whether to party or not. A careless programmer might forget to do the check and party even with an ill-formed pie.

So we ask ourselves: is it possible to stop the execution not just of the function, but of the whole program when we find an unforeseen situation?

To improve on our previous attempt, we can use the exceptions. To tell Python to interrupt the program execution in a given point, we can insert the instruction raise like this:

raise Exception()

If we want, we can also write a message to help programmers (who could be ourselves …) to understand the problem origin. In our case it could be a message like this:

raise Exception("Don't have enough flour !")

Note: in professional programs, the exception messages are intended for programmers, verbose, and tipically end up hidden in system logs. To final users you should only show short messages which are understanble by a non-technical public. At most, you can add an error code which the user might give to the technician for diagnosing the problem.

EXERCISE: Try to rewrite the function above by substituting the rows containing return with raise Exception():

Show solution
[4]:
def make_exceptional_pie(milk, sugar, flour):
    """ - suppose we need 1.3 kg for milk, 0.2kg for sugar and 1.0kg for flour

        - takes as parameters the quantities we have in the sideboard

        - if there are missing ingredients, raises Exception

    """
    # implement function

Once implemented, by writing

make_exceptional_pie(5,1,0.3)
print("Party")

you should see the following (note how “Party” is not printed):

take milk
take sugar

---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-10-02c123f44f31> in <module>()
----> 1 make_exceptional_pie(5,1,0.3)
      2
      3 print("Party")

<ipython-input-9-030239f08ca5> in make_exceptional_pie(milk, sugar, flour)
     18         print("take flour")
     19     else:
---> 20         raise Exception("Don't have enough flour !")
     21     print("Mix")
     22     print("Heat")

Exception: Don't have enough flour !

We see the program got interrupted before arriving to mix step (inside the function), and it didn’t even arrived to party (which is outside the function). Let’s try now to call the function with enough ingredients in the sideboard:

[5]:
make_exceptional_pie(5,1,20)
print("Party")
take milk
take sugar
take flour
Mix
Heat
I made the pie !
Party

Manage exceptions

Instead of brutally interrupting the program when problems are spotted, we might want to try some alternative (like go buying some ice cream). We could use some try except blocks like this:

[6]:
try:
    make_exceptional_pie(5,1,0.3)
    print("Party")
except:
    print("Can't make the pie, what about going out for an ice cream?")
take milk
take sugar
Can't make the pie, what about going out for an ice cream?

If you note, the execution jumped the print("Party" but no exception has been printed, and the execution passed to the row right after the except

Particular exceptions

Until know we used a generic Exception, but, if you will, you can use more specific exceptions to better signal the nature of the error. For example, when you implement a function, since checking the input values for correctness is very frequent, Python gives you an exception called ValueError. If you use it instead of Exception, you allow the function caller to intercept only that particular error type.

If the function raises an error which is not intercepted in the catch, the program will halt.

[7]:

def make_exceptional_pie_2(milk, sugar, flour):
    """ - suppose we need 1.3 kg for milk, 0.2kg for sugar and 1.0kg for flour

        - takes as parameters the quantities we have in the sideboard

        - if there are missing ingredients, raises Exception
    """

    if milk > 1.3:
        print("take milk")
    else:
        raise ValueError("Don't have enough milk !")
    if sugar > 0.2:
        print("take sugar")
    else:
        raise ValueError("Don't have enough sugar!")
    if flour > 1.0:
        print("take flour")
    else:
        raise ValueError("Don't have enough flour!")
    print("Mix")
    print("Heat")
    print("I made the pie !")

try:
    make_exceptional_pie_2(5,1,0.3)
    print("Party")
except ValueError:
    print()
    print("There must be a problem with the ingredients!")
    print("Let's try asking neighbors !")
    print("We're lucky, they gave us some flour, let's try again!")
    print("")
    make_exceptional_pie_2(5,1,4)
    print("Party")
except:  # manages all exceptions
    print("Guys, something bad happened, don't know what to do. Better to go out and take an ice-cream !")

take milk
take sugar

There must be a problem with the ingredients!
Let's try asking neighbors !
We're lucky, they gave us some flour, let's try again!

take milk
take sugar
take flour
Mix
Heat
I made the pie !
Party

For more explanations about try catch, you can see Real Python - Python Exceptions: an Introduction

assert

They asked you to develop a program to control a nuclear reactor. The reactor produces a lot of energy, but requires at least 20 meters of water to cool down, and your program needs to regulate the water level. Without enough water, you risk a meltdown. You do not feel exactly up to the job, and start sweating.

Nervously, you write the code. You check and recheck the code - everything looks fine.

On inauguration day, the reactor is turned on. Unexpectedly, the water level goes down to 5 meters, and an uncontrolled chain reaction occurs. Plutoniom fireworks follow.

Could we have avoided all of this? We often believe everything is good but then for some reason we find variables with unexpected values. The wrong program described above might have been written like so:

[8]:
# we need water to cool our reactor

water_level = 40 #  seems ok

print("water level: ", water_level)


# a lot of code

# a lot of code

# a lot of code

# a lot of code

water_level = 5  # forgot somewhere this bad row !

print("WARNING: water level low! ", water_level)

# a lot of code

# a lot of code

# a lot of code

# a lot of code

# after a lot of code we might not know if there are the proper conditions so that everything works allright

print("turn on nuclear reactor")
water level:  40
WARNING: water level low!  5
turn on nuclear reactor

How could we improve it? Let’s look at the assert command, which must be written by following it with a boolean condition.

assert True does absolutely nothing:

[9]:
print("before")
assert True
print("after")
before
after

Instead, assert False completely blocks program execution, by launching an exception of type AssertionError (Note how "after" is not printed):

print("before")
assert False
print("after")
before
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-7-a871fdc9ebee> in <module>()
----> 1 assert False

AssertionError:

To improve the previous program, we might use assert like this:

# we need water to cool our reactor

water_level = 40   # seems ok

print("water level: ", water_level)


# a lot of code

# a lot of code

# a lot of code

# a lot of code

water_level = 5  # forgot somewhere this bad row !

print("WARNING: water level low! ", water_level)

# a lot of code

# a lot of code

# a lot of code

# a lot of code

# after a lot of code we might not know if there are the proper conditions so that
# everything works allright so before doing critical things, it is always a good idea
# to perform a check ! if asserts fail (that is, the boolean expression is False),
# the execution suddenly stops

assert water_level >= 20

print("turn on nuclear reactor")
water level:  40
WARNING: water level low!  5

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-3-d553a90d4f64> in <module>
     31 # the execution suddenly stops
     32
---> 33 assert water_level >= 20
     34
     35 print("turn on nuclear reactor")

AssertionError:

When to use assert?

The case above is willingly exagerated, but shows how a check more sometimes prevents disasters.

Asserts are a quick way to do checks, so much so that Python even allows to ignore them during execution to improve the performance (calling python with the -O parameter like in python -O my_file.py).

But if performance are not a problem (like in the reactor above), it’s more convenient to rewrite the program using an if and explicitly raising an Exception:

# we need water to cool our reactor

water_level = 40   # seems ok

print("water level: ", water_level)


# a lot of code

# a lot of code

# a lot of code

# a lot of code

water_level = 5  # forgot somewhere this bad row !

print("WARNING: water level low! ", water_level)

# a lot of code

# a lot of code

# a lot of code

# a lot of code

# after a lot of code we might not know if there are the proper conditions so
# that everything works all right. So before doing critical things, it is always
# a good idea to perform a check !

if water_level < 20:
    raise Exception("Water level too low !")  # execution stops here

print("turn on nuclear reactor")
water level:  40
WARNING: water level low!  5

---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-30-4840536c3388> in <module>
     30
     31 if water_level < 20:
---> 32     raise Exception("Water level too low !")  # execution stops here
     33
     34 print("turn on nuclear reactor")

Exception: Water level too low !

Note how the reactor was not turned on.

Testing

  • If it seems to work, then it actually works? Probably not.

  • The devil is in the details, especially for complex algorithms.

  • We will do a crash course on testing in Python

WARNING: Bad software can cause losses of million $/€ or even harm people. Suggested reading: Software Horror Stories

Where Is Your Software?

As a data scientist, you might likely end up with code which is moderately complex from an algorithmic point of view, but maybe not too big in size. Either way, when red line is crossed you should start testing properly:

where is your software

Testing with asserts

NOTE: in this book we test with assert, but there are much better frameworks for testing!

If you get serious about software development, please consider using something like PyTest (recent and clean) or Unittest (Python default testing suite, has more traditional approach)

In the part about Python Foundations - A.3 Algorithms, we often use assert to perfom tests, that is, to verify a function behaves as expected.

Look for example at this function:

[10]:
def my_sum(x, y):
    s = x + y
    return s

We expect that my_sum(2,3) gives 5. We can write in Python this expectation by using an assert:

[11]:
assert my_sum(2,3) == 5

Se my_sum is correctly implemented:

  1. my_sum(2,3) will give 5

  2. the boolean expression my_sum(2,3) == 5 will give True

  3. assert True will be exectued without producing any result, and the program execution will continue.

Otherwise, if my_sum is NOT correctly implemented like in this case:

def my_sum(x,y):
    return 666
  1. my_sum(2,3) will produce the number 666

  2. the boolean expression my_sum(2,3) == 5 will giveFalse

  3. assert False will interrupt the program execution, raising an exception of type AssertionError

Exercise structure

Exercises in the Foundations - A.3 Algorithms are often structured in the following format:

def my_sum(x,y):
    """ RETURN the sum of numbers x and y
    """
    raise Exception("TODO IMPLEMENT ME!")


assert my_sum(2,3) == 5
assert my_sum(3,1) == 4
assert my_sum(-2,5) == 3

If you attempt to execute the cell, you will see this error:

---------------------------------------------------------------------------
Exception                                 Traceback (most recent call last)
<ipython-input-16-5f5c8512d42a> in <module>()
      6
      7
----> 8 assert my_sum(2,3) == 5
      9 assert my_sum(3,1) == 4
     10 assert my_sum(-2,5) == 3

<ipython-input-16-5f5c8512d42a> in somma(x, y)
      3     """ RETURN the sum of numbers x and y
      4     """
----> 5     raise Exception("TODO IMPLEMENT ME!")
      6
      7

Exception: TODO IMPLEMENT ME!

To fix them, you will need to:

  1. substitute the row raise Exception("TODO IMPLEMENT ME!") with the body of the function

  2. execute the cell

If cell execution doesn’t result in raised exceptions, perfect ! It means your function does what it is expected to do (the assert which succeed do not produce any output)

Otherwise, if you see some AssertionError, probably you did something wrong.

NOTE: The raise Exception("TODO IMPLEMENT ME") is put there to remind you that the function has a big problem, that is, it doesn’t have any code !!! In long programs, it might happen you know you need a function, but in that moment you don’t know what code put in th efunction body. So, instead of putting in the body commands that do nothing like print() or pass or return None, it is WAY BETTER to raise exceptions so that if by chance the program reaches the function, the execution is suddenly stopped and the user is signalled with the nature and position of the problem. Many editors for programmers, when automatically generating code, put inside function skeletons to implement some Exception like this.

Let’s try to willingly write a wrong function body, which always return 5, independently from x and y given in input:

def my_sum(x,y):
    """ RETURN the sum of numbers x and y
    """
    return 5

assert my_sum(2,3) == 5
assert my_sum(3,1) == 4
assert my_sum(-2,5) == 3

In this case the first assertion succeeds and so the execution simply passes to the next row, which contains another assert. We expect that my_sum(3,1) gives 4, but our ill-written function returns 5 so this assert fails. Note how the execution is interrupted at the second assert:

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
<ipython-input-19-e5091c194d3c> in <module>()
      6
      7 assert my_sum(2,3) == 5
----> 8 assert my_sum(3,1) == 4
      9 assert my_sum(-2,5) == 3

AssertionError:

If we implement well the function and execute the cell we will see no output: this means the function successfully passed the tests and we can conclude that it is correct with reference to the tests:

ATTENTION: always remember that these kind of tests are never exhaustive ! If tests pass it is only an indication the function might be correct, but it is never a certainty !

[12]:

def my_sum(x,y):
    """ RETURN the sum of numbers x and y
    """
    return x + y

assert my_sum(2,3) == 5
assert my_sum(3,1) == 4
assert my_sum(-2,5) == 3

EXERCISE: Try to write the body of the function multiply:

  • substitute raise Exception("TODO IMPLEMENT ME") with return x * y and execute the cell. If you have written correctly, nothing should happen. In this case, congratulatins! The code you have written is correct with reference to the tests !

  • Try to substitute instead with return 10 and see what happens.

Show solution
[13]:
def my_mul(x,y):
    """ RETURN the multiplication of numbers x and y
    """
    raise Exception('TODO IMPLEMENT ME !')


assert my_mul(2,5) == 10
assert my_mul(0,2) == 0
assert my_mul(3,2) == 6

Exercise - gre3

✪✪ Write a function gre3 which takes three numbers and RETURN the greatest among them

Examples:

>>> gre3(1,2,4)
4

>>> gre3(5,7,3)
7

>>> gre3(4,4,4)
4
Show solution
[14]:


assert gre3(1,2,4) == 4
assert gre3(5,7,3) == 7
assert gre3(4,4,4) == 4

Exercise - final_price

✪✪ The cover price of a book is € 24,95, but a library obtains 40% of discount. Shipping costs are € 3 for first copy and 75 cents for each additional copy. How much n copies cost ?

Write a function final_price(n) which RETURN the price.

ATTENTION 1: For numbers Python wants a dot, NOT the comma !

ATTENTION 2: If you ordered zero books, how much should you pay ?

HINT: the 40% of 24,95 can be calculated by multiplying the price by 0.40

>>> p = final_price(10)
>>> print(p)

159.45

>>> p = final_price(0)
>>> print(p)

0
Show solution
[15]:
def final_price(n):
    raise Exception('TODO IMPLEMENT ME !')

assert final_price(10) == 159.45
assert final_price(0) == 0

Exercise - arrival_time

✪✪✪ By running slowly you take 8 minutes and 15 seconds per mile, and by running with moderate rhythm you take 7 minutes and 12 seconds per mile.

Write a function arrival_time(n,m) which, supposing you start at 6:52, given n miles run with slow rhythm and m with moderate rhythm, PRINTs arrival time.

  • HINT 1: to calculate an integer division, use//

  • HINT 2: to calculate the reminder of integer division, use the module operator %

>>> arrival_time(2,2)
7:22
Show solution
[16]:
def arrival_time(n,m):
    raise Exception('TODO IMPLEMENT ME !')


assert arrival_time(0,0) == '6:52'
assert arrival_time(2,2) == '7:22'
assert arrival_time(2,5) == '7:44'
assert arrival_time(8,5) == '8:34'
assert arrival_time(40,5) == '12:58'
assert arrival_time(100,25) == '23:37'
assert arrival_time(100,40) == '1:25'
assert arrival_time(700,305) == '19:43' # Forrest Gump

Continue

Go on with exercises about functions and strings

[ ]: