Python Basics
Error handling
In any given language, errors are a feature that let you know that something that you might not have expected occured. It is important to know how to handle and use them correctly.
Tristan
contact-datartichaut@pm.me
Python projects list
Beginner
What are errors good for ?
When we say that errors are a feature that let you know that something that you might not have expected occured, there are a few key words in this sentence :
Let's look at an example of error handling.
ToC
value_N1 = 0
value_N2 = 20
try:
growth = value_N2 / value_N1
except ZeroDivisionError:
growth = None
print('The N+2 value is 0, therefore growth cannot be calculated.')
The try
block contains code that is expected to throw an error, the
except
block catches the error
and deals with it.
Before getting into why and how to handle errors properly, there are two important concepts to go over.
Beginner
It is Easier to Ask for Forgiveness than Permission.
When working with python, there is this concept that it is "easier to ask for forgiveness than permission". This is opposed to another programming concept named "look before you leap".
Let's look at an example of "look before you leap" code. Let's assume we have values for a year N2 and want to know the growth from N1. For some reason, some values for N1 are missing, maybe they did not exist a year prior. With the "LBYL" approach, we would do that :
ToC
# Two lists of values.
lst_N1 = [10, 25, 0, 6, 24, 15, 0]
lst_N2 = [8, 28, 15, 6, 21, 18, 7]
# Putting them by pair with the built-in zip function.
pairs = list(zip(lst_N1, lst_N2))
# Iterating through the pairs and building a list of calculated growth.
growth = []
for pair in pairs:
# First we test if the division is going to be possible ...
if pair[0] != 0:
# ... Then and only then do we compute the growth.
growth.append(pair[1] / pair[0])
else:
# If the division is not possible we just append None.
growth.append(None)
print(growth)
This approach is called "look before you leap" because before every division we test if the second value is 0 or not, when we are sure the division can be called safely, then we divide.
This method is not wrong, but is not the most efficient way of handeling things. In most cases, when attempting something, we expect it to succed. In this example most values are strictly positive integers but we have to check for the rare occasion where it is in fact 0.
Let's do the same operation with the "EAFP" method.
# Two lists of values.
lst_N1 = [10, 25, 0, 6, 24, 15, 0]
lst_N2 = [8, 28, 15, 6, 21, 18, 7]
# Putting them by pair with the built-in zip function.
pairs = list(zip(lst_N1, lst_N2))
# Iterating through the pairs and building a list of calculated growth.
growth = []
for pair in pairs:
try:
# The division is tried no matter what.
growth.append(pair[1] / pair[0])
except ZeroDivisionError:
# If the division throws an error, we append None instead.
growth.append(None)
print(growth)
The result is the same except a lot less tests (if
statements) are performed in this code.
Beginner
Duck typing.
Duck typing is another important concept related to error handling. The gist of it is : "if it quacks like a duck, and fly like a duck then assume it is a duck".
The underlying concept here is that if an object can do the thing you want it to do then you do not have to worry about its type.
Let's see an example.
ToC
l1 = [1, 2, 3, 4]
print(sum(l1))
s1 = {1, 2, 3, 4}
print(sum(s1))
import pandas as pd
serie_1 = pd.Series([1, 2, 3, 4])
print(sum(serie_1))
Here we can see that the built-in sum
function works with any object that behaves like a collection
of numbers, even on a pandas.Series
that is not a buily-in type.
"If it quack like a duck, treat it like a duck".
This notion is related to the "EAFP" method and error handling because, just like before, we don't lose time with checking if we can do something, rather we try to do it and then handle the case where it does not work.
Beginner
Error handling.
With all that information, it is easier to understand why error handling is important :
Now let's look at how to handle errors, with an example.
ToC
try:
file = open('file.txt', 'r')
# Do stuff.
file.close()
except FileNotFoundError:
print('The file was not found but the program does not need to be stopped here.')
print('The program continues here.')
This example, with the same structure as our first example, can be extended like so :
try:
file = open('file.txt', 'r')
except FileNotFoundError:
print('The file was not found but the program does not need to be stopped here.')
else:
str_text = file.read()
print(str_text)
file.close()
finally:
print('This is printed no matter what.')
In this example the main blocks are :
try
: contains everything that might throw an error that we want to catch.except
: handle a specific error. An except
block can handle multiple
errors, and a try
statement can have multiple except
statements.else
: contains everything that must be executed after the try
statement.
It is possible to write everything inside the try
block but using an else
statement is cleaner.finally
: contains everything that needs to be executed no matter what.Note : in that example an error can still occur in the else
statement which would prevent
the close
method to be called. The closed
method itself cannot be moved as is
in the finally
block because it would be executed even if the file does not exists.
This example can be made a little more complex.
# we try to access the file without checking if it is possible or not.
try:
file = open('file.txt', 'r')
# If the file does not exist, we print a message and do nothing else.
except FileNotFoundError:
print('The file was not found but the program does not need to be stopped here.')
else:
# If the file does exist then we try to read it.
try:
str_text = file.read()
print(str_text)
# If anything happen during the reading of the file, we close it.
finally:
file.close()
finally:
print('This is printed no matter what.')
A last, a more complex example just to show what is possible to do.
# Code that can throw an error.
try:
pass
# A very specific except clause.
except ZeroDivisionError:
pass
# A slightly more generic except clause.
except (AttributeError, NameError) as e:
pass
# A very generic except clause.
except Exception as e:
pass
# Code that is executed if no exception is caught.
else:
pass
# Code that is executed no matter what.
finally:
pass
When using multiple except
block, the more specific should go first because otherwise an error
can be caught in a wider except
statement.
Also using a bare except:
statement is not recommended not only because you might catch unwanted
errors but also because it catchs KeyboardInterrupt
errors, and prevent the user to stop a program
caught in an infinite loop, for example.