Error handling and testing solutions
Download exercises zip
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
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 !
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
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:
take milk
take sugar
take flour
mix
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:
interrupt the execution flow of the program
signal to whoever called the function that a problem has occurred
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 answerCheck 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()
:
[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:
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 Foundations - A.3 Basic 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:
my_sum(2,3)
will give5
the boolean expression
my_sum(2,3) == 5
will giveTrue
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
my_sum(2,3)
will produce the number666
the boolean expression
my_sum(2,3) == 5
will giveFalse
assert False
will interrupt the program execution, raising an exception of typeAssertionError
Exercise structure
Exercises in the Foundations - A.3 Basic 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:
substitute the row
raise Exception("TODO IMPLEMENT ME!")
with the body of the functionexecute 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")
withreturn 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.
[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
[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
[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
[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
[ ]: