Exceptions and Exception Handling

Introduction

This unit addresses the almost inevitable occurence of unanticipated errors in code, and methods to detect and handle exceptions.

Note: This “notebook plus modules” organization is useful when the collection of function definitions usd in a project is lengthy, and would otherwise clutter up the notebook and hamper readability.

Handing and Raising Exceptions

Ideally, when we write a computer program for a mathematical task, we will plan in advance for every possibility, and design an algorithm to handle all of them. For example, anticipating the possibility of division by zero and avoiding it is a common issue in making a program robust.

However this is not always feasible; in particular while still developing a program, there might be situations that you have not yet anticipated, and so it can be useful to write a program that will detect problems that occur while the program is running, and handle them in a reasonable way.

We start by considering a very basic code for our favorite example, solving quadratic equations.

from math import sqrt  
def quadratic_roots(a, b, c):
    root_of_discriminant = sqrt(b**2 - 4*a*c)
    return ( (-b - root_of_discriminant)/(2*a),  (-b + root_of_discriminant)/(2*a) )
# An easy and familiar test case
(a, b, c) = (2, -10, 8)
print("Let's solve the quadratic equation %g*x**2 + %g*x + %g = 0" % (a, b, c))
(root0, root1) = quadratic_roots(a, b, c)
print("The roots are %g and %g" % (root0, root1))
Let's solve the quadratic equation 2*x**2 + -10*x + 8 = 0
The roots are 1 and 4

Try it repeatedly, with some “destructive testing”: seek input choices that will cause various problems.

For this, it is is useful to have an interactive loop to ask for test cases:

# Testing: add some hard cases, interactively
print("Let's solve some quadratic equations a*x**2 + b*x + c = 0")
keepgoing = True
while keepgoing: 
    a = float(input("a = "))
    b = float(input("b = "))
    c = float(input("c = "))
    print("Solving the quadratic equation %g*x**2 + %g*x + %g = 0" % (a, b, c))
    (root0, root1) = quadratic_roots(a, b, c)
    print("The roots are %g and %g" % (root0, root1))
    yesorno = input("Do you want to try another case? [Answer y/n]: ")
    keepgoing = yesorno[0] in {'y','Y'}  # examine the first letter only!

Let me know what problems you found; we will work on detecting and handling all of them.

Some messages I get ended with these lines, whose meaning we will explore:

  • ZeroDivisionError: float division by zero

  • ValueError: math domain error

  • ValueError: could not convert string to float …

Catching any “exceptional” situation and handling it specially

Here is a minimal way to catch all problems, and at least apologize for failing to solve the equation:

# Exception handling, version 1
print("Let's solve some quadratic equations a*x**2 + b*x + c = 0")
keepgoing = True
while keepgoing: 
    try:
        a = float(input("a = "))
        b = float(input("b = "))
        c = float(input("c = "))
        print("Solving the quadratic equation %g*x**2 + %g*x + %g = 0" % (a, b, c))
        (root0, root1) = quadratic_roots(a, b, c)
        print("The roots are %g and %g" % (root0, root1))
    except:
        print("Something went wrong; sorry!")
    yesorno = input("Do you want to try another case? [Answer y/n]: ")
    keepgoing = yesorno[0] in {'y','Y'}    # examine the first letter only, so "nevermore" means "no"

This try/except structure does two things:

  • it first tries to run the code in the (indented) block introduced by the colon after the statement try

  • if anything goes wrong (in Python jargon, if any exception occurs) it gives up on that try block and runs the code in the block under the statement except.

Catching and displaying the exception error message

One thing has been lost though: the messages like “float division by zero” as seen above, which say what sort of exception occured.

We can regain some of that, by having except statement save that message into a variable:

# Exception handling, version 2: displaying the exception

print("Let's solve some quadratic equations a*x**2 + b*x + c = 0")
keepgoing = True
while keepgoing: 
    try:
        a = float(input("a = "))
        b = float(input("b = "))
        c = float(input("c = "))
        print("Solving the quadratic equation %g*x**2 + %g*x + %g = 0" % (a, b, c))
        (root0, root1) = quadratic_roots(a, b, c)
        print("The roots are %g and %g" % (root0, root1))
    except Exception as what_just_happened:
        print("Something went wrong; sorry!")
        print("The exception is: ", what_just_happened)
        print("The exception message is: ", what_just_happened.args[0])
        #if what_just_happened == "float division by zero":
        #if what_just_happened[:5] == "float":
        #    print("You cannot divide by zero.")
    yesorno = input("Do you want to try another case? [Answer y/n]: ")
    keepgoing = yesorno[0] in {'y','Y'}    # examine the first letter only, so "nevermore" means "no"

This version detects every possible exception and handles them all in the same way, whether it be a problem in arithmetic (like the dreaded division by zero) or the user making a typing error in the input of the coefficients. Try answering “one” when asked for a coefficient!

Handling specific exception types

Python divides exceptions into many types, and the statement except can be given the name of an exception type, so that it then handles only that type of exception.

For example, in the case of division be zero, where we originally got a message

ZeroDivisionError: float division by zero

we can catch that particular exception and handle it specially:

# Exception handling, version 3: as above, but with special handing for division by zero.

print("Let's solve some quadratic equations a*x**2 + b*x + c = 0")
keepgoing = True
while keepgoing:
    try:
        a = float(input("a = "))
        b = float(input("b = "))
        c = float(input("c = "))
        print("Solving the quadratic equation %g*x**2 + %g*x + %g = 0" % (a, b, c))
        (root0, root1) = quadratic_roots(a, b, c)
        print("The roots are %g and %g" % (root0, root1))
    except ZeroDivisionError as what_just_happened:  # Note the "CamelCase": capitalization of each word
        print("Division by zero; the first coefficient cannot be zero!")
        print("Please try again.")
    yesorno = input("Do you want to try another case? [Answer y/n]: ")
    keepgoing = yesorno[0] in {'y','Y'}  # examine the first letter only, so "nevermore" means "no"

Handling multiple exception types

However, this still crashes with other errors, lke typos in the input. To detect several types of exception, and handle each in an appropriate way, there can be a list of except statements, each with a block of code to run when that exception is detected. The type Exception was already seen above; it is just the totally generic case, so can be used as a catch-all after a list of exception types have been handled.

Experiment a bit, and you will see how these multiple except statements are used:

  • the first except clause that matches is used, and any later ones are ignored;

  • only if none matches does the code go back to simply “crashing”, as with version 0 above.

Summary: a while-try-except pattern for interactive programs

For programs with interactive input, a useful pattern for robustly handling errors or surprises in the input is a while-try-except pattern, with a form like:

try_again = True
while try_again:
    try:
        Get input
        Do stuff with it
        try_again = False
    except Exception as message:
        print("Exception", message, " occurred; please try again.")
        Maybe actually fix the problem, and if successful: try_again = False

One can refine this by adding except clauses for as many specific exception types as are relevant, with more specific handling for each.

Exercise D-A. Add handling for multiple types of exception

Copy your latest version of a “quadratic_solver” function here into your module name something like “math246” created for Unit 9) — or into a new file named something like quadratic_solvers.py. Then augment that function with multiple except clauses to handle all exceptions that we can get to occur.

First, read about the possibilities, for example in Section 5 of the official Python 3 Standard Library Reference Manual
at https://docs.python.org/3/library/exceptions.html, or other sources that you can find.

Two exceptions of particular importance for us are ValueError and ArithmeticError, and sub-types of the latter like ZeroDivisionError and OverflowError. (Note the “CamelCase” capitalization of each word in an exception name: it is essential to get this right, since Python is case-sensitive.)

Aside: If you find a source on Python exceptions that you prefer to the above references, please let us all know!

Exercise D-B. Handling division by zero in Newton’s method

Using a basic code for Newton’s Method (such as the one I provide in module root_finders) experiment with exception handling for the possibility of division by zero.

(You could then do likewise with the Secant Method.)