18. Exceptions
You have all seen various exceptions in Python
int('hello')
ValueError: invalid literal for int() with base 10: 'hello'
In the above example, when we tried to convert the string
"hello"to an integer, Python raised an exception
a = ['a', 'e', 'i', 'o', 'u']
print(a[11])
IndexError: list index out of range
In the above example, when we tried to access the 11th thing in the list containing only 5 things, Python raised an exception
Both
ValueErrorandIndexErrorare exceptions, but there are many more kinds of exceptionsConsider how, like the
printfunction, someone had to write the code for converting strings to integers and for indexing elements from a listIf I am the one writing the code for converting strings to integers, what should I make my code do if someone asks my code to convert the string
"hello"to an integer?There is no single natural way to convert the string
"hello"to an integerShould my code simply ignore the request and carry on like nothing ever happened?
Should my code crash the whole program?
Maybe some user entered some input wrong?
Maybe the issue is with some text file that was read?
…
The trouble is, if I am writing the code for converting strings to integers, I cannot possibly know what you — the individual trying to use my code at some point in the future — want to do in these exceptional situations
What I can do however is raise an exception, which then communicates to the programmer using my code that they have to specify what they want to do when the exceptional situation arises
Consider the example below on dividing by zero
1def divide(a,b):
2 if b == 0:
3 raise ZeroDivisionError("Wait, that's illegal")
4 else:
5 return a/b
Dividing a number by zero is going to be a problem
But what should happen if someone tries to?
That’s entirely up to the programmer making use of the
dividefunctionAll I need to do is communicate to them that something exceptional happened by
raise-ing an exceptionThe function first checks if
bis0If
bis0, then the exception israiseedIf it is not, then the function carries on as it should
18.1. Catching Exceptions
Pretend the above function
dividewas written in 1991 and you want to use it todayWhen you are using it today, you end up calling
divide(9,0), which is problematicOne cannot divide 9 by 0
What should the original programmer in 1991 do about it?
Well… not much considering you are the one trying to use the function today
How could the programmer know what to do to handle your specific situation today?
What the original programmer can do, however, is to add some special code that says something exceptional happened that lets you know that something is off
This then allows you, the individual trying to use
dividetoday, to deal with the situation the way you needCrash?
Read input again?
Carry on as if nothing happened?
If you are using a function that may result in an exception, you can write your code such that
You
tryto run the code that may or may notraisean exceptionThe code will run normally
exceptwhen the exceptional situation arisesIf the exceptional situation arises, special instructions will be specified
Below is a generalized idea of how one can do this
def my_code():
try:
might
cause
exception
except SomeError:
will
handle
exception
runs
regardless
The code in the
tryblock runs normally; if an exception is raised, control jumps to theexceptblock instead
18.1.1. Example 1
There exists a special value for floating point numbers in Python called
NaN, which means not a numberA reasonable way to manage a
ZeroDivisionErroris to use aNaNvalue
1def not_a_number_example(a: float, b: float) -> float:
2 try:
3 quotient = divide(a, b)
4 except ZeroDivisionError:
5 quotient = float("NaN")
6 return quotient
If
divideis called and there is noZeroDivisionError, then the division occurs and thequotientis returnedOn the other hand, if a
ZeroDivisionErrorhappens, we assignNaNtoquotientand return itEither way, the
quotientis returned
18.1.2. Example 2
Consider a program requiring a user to input some values
If this is the case, it may be ideal to have the program ask for input again if the input was inadmissible
1def continue_asking_for_input() -> float:
2 while True:
3 try:
4 data = input("Provide operands for division: ").split()
5 a = float(data[0])
6 b = float(data[1])
7 quotient = divide(a, b)
8 break
9 except ZeroDivisionError:
10 print("Cannot Divide By Zero --- Try Again")
11
12 return quotient
18.1.3. Example 3
It may be the case that if the function causes an exception, our program should stop running immediately
If this is the case, we can make use of the
exit()function to halt the program
1def stop_running_immediately(a,b) -> float:
2 try:
3 quotient = divide(a,b)
4 except ZeroDivisionError:
5 exit() # Immediately stop!
6 return quotient
A simpler and better approach is to just let the exception propagate — no try/except needed
1def let_it_propagate(a, b) -> float:
2 quotient = divide(a, b)
3 return quotient
If
divideraises an exception, it keeps being passed up the call stack until something handles it or the program crashes
18.1.4. Example 4
Sometimes the exception may be inconsequential for the program’s functionality
Perhaps your program is rapidly reading input from some sensors for calculations and a
ZeroDivisionErroris likely caused by sensor precisionGiven this, it may be the case that periodic
ZeroDivisionErrors are meaningless, so they can be ignored
1try:
2 quotient = divide(a, b)
3except ZeroDivisionError:
4 pass
18.2. Why Care?
Which of the above examples is the correct one?
The trouble is, that depends on your situation
The point is, how can the programmer in 1991 know what you want to do with your situation today?
Ultimately, exceptions are useful because
They allow programmers to pass info around and communicate through time
They allow us to deal with exceptional situations effectively
They provide a nice logical division between normal code and exceptional code