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.
Unforeseen situations#
There is a party for a birthday and these lazy friends of yours 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’s late, and there isn’t any open supermarket. 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:
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?
Answer
The program above announces the party even when we do not have enough ingredients!
Check with the return#
EXERCISE: We could correct the problems of the above pie by adding return
commands.
Improve make_pie
above by returning True if the pie is doable, and False otherwise, so that outside of the function, you may use the value returned to announce the party or not.
Warning
Do not move the print("Party")
inside the function.
The goal of the exercise is to keep it outside, so to use the value returned by make_pie
in order to decide whether to announce the party or not.
Show code cell content
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
"""
if milk > 1.3:
print("take milk")
# return True # NO, it would finish right here
else:
print("Don't have enough milk !")
return False
if sugar > 0.2:
print("take sugar")
else:
print("Don't have enough sugar !")
return False
if flour > 1.0:
print("take flour")
else:
print("Don't have enough flour !")
return False
print("Mix")
print("Heat")
print("I made the pie !")
return True
made_pie = make_pie(5,1,0.3)
if made_pie:
print("Party")
else:
print("No party !")
take milk
take sugar
Don't have enough flour !
No party !
Exceptions#
Using return
we improved the previous function, but there remains a problem: the responsibility 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 announce the party or not. A careless programmer might forget to do the check and announce it even with an ill-formed pie.
So would it be 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 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 origin of the problem. In our case it could be a message like this:
raise Exception("Don't have enough flour !")
EXERCISE: Try to rewrite the function above by substituting the rows containing return
with raise Exception()
:
Show code cell content
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
"""
if milk > 1.3:
print("take milk")
else:
raise Exception("Don't have enough milk !")
if sugar > 0.2:
print("take sugar")
else:
raise Exception("Don't have enough sugar!")
if flour > 1.0:
print("take flour")
else:
raise Exception("Don't have enough flour!")
print("Mix")
print("Heat")
print("I made the pie !")
make_exceptional_pie(5, 1, 0.3)
print("Party")
take milk
take sugar
---------------------------------------------------------------------------
Exception Traceback (most recent call last)
Cell In[5], line 1
----> 1 make_exceptional_pie(5, 1, 0.3)
2 print("Party")
Cell In[4], line 20, 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:
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:
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.
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:
# we need water to cool our reactor
water_level = 40 # seems ok
print("water level: ", water_level)
# a lot of code
water_level = 5 # forgot somewhere this bad row !
print("WARNING: water level low! ", water_level)
# a lot of code
# after a lot of code we might not know if there are the proper conditions so that everything works alright
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:
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)
Cell In[11], line 2
1 print("before")
----> 2 assert False
3 print("after")
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
water_level = 5 # forgot somewhere this bad row !
print("WARNING: water level low! ", water_level)
# a lot of code
# after a lot of code we might not know if there are the proper conditions so that
# everything works alright 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)
Cell In[12], line 20
11 print("WARNING: water level low! ", water_level)
13 # a lot of code
14
15 # after a lot of code we might not know if there are the proper conditions so that
16 # everything works alright so before doing critical things, it is always a good idea
17 # to perform a check ! if asserts fail (that is, the boolean expression is False),
18 # the execution suddenly stops
---> 20 assert water_level >= 20
22 print("turn on nuclear reactor")
AssertionError:
When to use assert?#
The case above is willingly exaggerated, but shows how an additional check 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 is 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
water_level = 5 # forgot somewhere this bad row !
print("WARNING: water level low! ", water_level)
# a lot of code
# after a lot of code we might not know if there are the proper conditions so
# that everything works alright. 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)
Cell In[13], line 20
13 # a lot of code
14
15 # after a lot of code we might not know if there are the proper conditions so
16 # that everything works alright. So before doing critical things, it is always
17 # a good idea to perform a check !
19 if water_level < 20:
---> 20 raise Exception("Water level too low !") # execution stops here
22 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
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)
During the remainder of this course, we will often use assert
to perform tests, that is, to verify a function behaves as expected.
Look for example at this function:
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
:
assert my_sum(2, 3) == 5
If 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 executed 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 - gre3#
✪✪ Write a function gre3
which takes three numbers and RETURN the greatest among them
Show code cell content
def gre3(a, b, c):
if a > b:
if a > c:
return a
else:
return c
else:
if b > c:
return b
else:
return c
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: Decimal numbers in Python contain a dot, NOT a 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
Show code cell content
def final_price(n):
if n == 0:
return 0
else:
return n * 24.95 * 0.6 + 3 + (n - 1) * 0.75
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 modulo operator
%
Show code cell content
def arrival_time(n, m):
start_hour = 6
start_minutes = 52
# past time
seconds = (
start_hour * 60 * 60
+ start_minutes * 60
+ n * (8 * 60 + 15)
+ m * (7 * 60 + 12)
)
minutes = seconds // 60
hours = minutes // 60
hours_display = hours % 24
minutes_display = minutes % 60
return "%s:%s" % (hours_display, minutes_display)
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