Lesson 5, Bit 5: Exception Handling and Debugging

We saw way back in Lesson 1 that a user can cause a bit of chaos with your program.  If your code prompts them for a number and they enter a letter, then you try to convert that letter into a number, an error will be thrown:

>>> age = input('How old are you'?)
How old are you? Too old!
>>> int(age)
ValueError: invalid literal for int() with base 10

Your script immediately stops in its tracks with a traceback. It does not continue and your user is left wondering what happened.

In your practice coding over the last few units, you have probably thrown several unintentional errors yourself.  Here is a list of common ones in case you missed them:

Exception Meaning
IndexError

Raised when a sequence subscript is out of range.

NameError

Raised when a local or global name is not found. This applies only to unqualified names. The associated value is an error message that includes the name that could not be found.

OSError

This exception is raised when a system function returns a system-related error, including I/O failures such as "file not found" or "disk full" (not for illegal argument types or other incidental errors).

RuntimeError

Raised when an error is detected that doesn’t fall in any of the other categories. The associated value is a string indicating what precisely went wrong.

TypeErrorp

Raised when an operation or function is applied to an object of inappropriate type. The associated value is a string giving details about the type mismatch.

ValueError

Raised when a built-in operation or function receives an argument that has the right type but an inappropriate value, and the situation is not described by a more precise exception such as IndexError.

ZeroDivisionError

Raised when the second argument of a division or modulo operation is zero. The associated value is a string indicating the type of the operands and the operation.

Here is a sample program which asks a user to enter how many pieces of pie they want, then displays what percentage of the whole pie they will get:

pieces = input("How many pieces of pie do you want? ")
percentage = 1/int(pieces)
print("You get", format(percentage, ".2%"), "of the pie!")

If we execute this code and give it good code, it the output looks something like this:

How many pieces of pie do you want? 2
That's 50.00% of the pie!

If we execute the code and give it invalid input, it simply fails with an unfriendly error message:

How many pieces of pie do you want? one

Traceback (most recent call last):
  File "example.py", line 1, in <module>
    pieces = int(input("How many pieces of pie do you want? "))
ValueError: invalid literal for int() with base 10: 'one'

And the program just stops.

We need a better way to handle these exceptions and allow the program to correct the issue or skip the code which is affected.

The try / except Statement

There is a conditional execution structure built into Python to handle these types of expected and unexpected errors called "try / except".

The idea of try and except is that you know that there is the potential for some sequence of instruction(s) to have a problem and you want to add some statements to be executed if an error occurs. These extra statements (the except block) are ignored if there is no error.

You can think of the try and except feature in Python as an "insurance policy" on a sequence of statements.

We can rewrite our temperature converter as follows:

pieces = input("How many pieces of pie do you want? ")

try:
    percentage = 1/int(pieces)
    print("You get", format(percentage, ".2%"), "of the pie!")

except:
    print("You need to enter a number!")

How does this work?  Python starts by executing the sequence of statements in the try block. If all goes well, it skips the except block and proceeds. If an exception occurs in the try block, Python jumps out of the try block and executes the sequence of statements in the except block.  The except block is known as the handler because it handles the exception.

How many pieces of pie do you want? One
You need to enter a number!

Handling an exception with a try statement is called catching an exception. In this example, the except clause prints an error message. In general, catching an exception gives you a chance to fix the problem, or try again, or at least end the program gracefully.

Launch Exercise

Displaying Exceptions

Sometimes we want to see the exception being thrown.   Going back to our example with pie: what if we enter a zero for our input?

How many pieces of pie do you want? 0
You need to enter a number!

What happened here?  A zero IS a valid number.  Why is an error being thrown?  We can do a couple of things to figure out which error was caught by the except block. 

First, you assign the exception to a variable like this:

pieces = input("How many pieces of pie do you want? ")

try:
    percentage = 1/int(pieces)
    print("You get", format(percentage, ".2%"), "of the pie!")

except Exception as my_exception:
    print(my_exception)
    print("You need to enter a number!")

Here we have explicitly called on the Exception object and assigned it to the variable, my_exception.  This results in this output:

How many pieces of pie do you want? 0
division by zero
You need to enter a number!

Ah ha!  Our problem is related to division by 0. 

Launch Exercise

The second way you can figure out which error was thrown is to the raise statement.  The raise statement allows you to intentionally "raise" an exception.  It can take a specific exception type as an argument, but when it is just called by itself, it will display the most recently thrown error.  Note: the program will stop once the raise statement is called.  This is best used only when troubleshooting.

pieces = input("How many pieces of pie do you want? ")

try:
    percentage = 1/int(pieces)
    print("You get", format(percentage, ".2%"), "of the pie!")

except:
    raise
    print("You need to enter a number!")

Now when we enter a zero, we get this:

How many pieces of pie do you want? 0

Traceback (most recent call last):
   File "example.py", line 3, in <module>
    percentage = 1/int(pieces)
ZeroDivisionError: division by zero

Our second way also narrowed it down to a division by zero error - or ZeroDivisionError.

Specifying the Exception

Obviously, telling the end user that they need to enter a number when zero IS a valid number is not a good solution.  We need special messages depending on the situation.  Fortunately, Python has you covered.

A try statement may have more than one except clause, to specify handlers for different exceptions.  This is useful for when you want to handle one type of error one way and a different type of error a different way.

Here's how we use it in the pie example:

pieces = input("How many pieces of pie do you want? ")

try:
    percentage = 1/int(pieces)
    print("You get", format(percentage, ".2%"), "of the pie!")

except ValueError:
    print("You need to enter a number!")

except ZeroDivisionError:
    print("You cannot enter a zero!")

Notice how the exception type is specified immediately after the except declaration.  When we run this code, here are our outputs:

How many pieces of pie do you want? 2
You get 50.00% of the pie!
How many pieces of pie do you want? One
You need to enter a number!
How many pieces of pie do you want? 0
You cannot enter a zero!

Now both of our known exceptions are covered.

When you have multiple except handlers, at most one handler will be executed.

You can also assign the contents of the error to a variable for use within the except block:

except ValueError as my_error:except ZeroDivisionError as division_error:

Launch Exercise

Specifying Multiple Exceptions at Once:

A single except clause may handle multiple types of exceptions:

except (NameError, ValueError, TypeError):

This is useful (and more efficient) if you want to handle different exception types the same way.

pieces = input("How many pieces of pie do you want? ")

try:
    percentage = 1/int(pieces)
    print("You get", format(percentage, ".2%"), "of the pie!")

except (ValueError, TypeError):
    print("You need to enter a number!")

except ZeroDivisionError:
    print("You cannot enter a zero!")

In this example, when ValueError or TypeError are thrown, they will both be caught by the same handler and You need to enter a number! will display.

Default Handling

Going back to our pie example, we are now handling very specific error types, but it is good practice to include the default except block in case something unexpected occurs.

pieces = input("How many pieces of pie do you want? ")

try:
    percentage = 1/int(pieces)
    print("You get", format(percentage, ".2%"), "of the pie!")

except (NameError, ValueError, TypeError):
    print("You need to enter a number!")

except ZeroDivisionError:
    print("You cannot enter a zero!")

except:
    print("Something bad happened. No pie for you.")

It should be noted that handlers only handle exceptions that occur in the corresponding try clause, not in other handlers of the same try statement.   This means that if you raise a second exception while handling the first exception, your original try / except statement will not be able to handle the second exception.  You would have to create a new, nested try / except statement within your exception handing.

Launch Exercise

else Clause

The try / except statement has an optional else clause.  The else clause comes after all except clauses and will only execute if the try clause does not raise an exception.

The use of the else clause is better than adding additional code to the try clause because it avoids accidentally catching an exception that wasn’t raised by the code being protected by the try / except statement.

Let's go back to pie!

pieces = input("How many pieces of pie do you want? ")

try:
    percentage = 1/int(pieces)

except (NameError, ValueError, TypeError):
    print("You need to enter a number!")

except ZeroDivisionError:
    print("You cannot enter a zero!")

except:
    print("Something bad happened. No pie for you.")

else:
    print("You get", format(percentage, ".2%"), "of the pie!")

In this case, we anticipate that the line percentage = 1/int(pieces) might throw errors if the input is not an integer or if it is equal to 0.  This is the only code which we want to catch our specific errors.  So, we move the line print("You get", format(percentage, ".2%"), "of the pie!") into the else clause.  The output looks identical:

How many pieces of pie do you want? 2
You get 50.00% of the pie!
How many pieces of pie do you want? One
You need to enter a number!
How many pieces of pie do you want? 0
You cannot enter a zero!

However, our program will now run cleaner with the extra code placed in the else clause.

Launch Exercise

Clean-up Actions (the finally Clause)

The try statement has another optional clause which is intended to define clean-up actions that must be executed under all circumstances.

A finally clause is always executed before leaving the try statement, whether an exception has occurred or not. When an exception has occurred in the try clause and has not been handled by an except clause (or it has occurred in an except or else clause), it is re-raised after the finally clause has been executed.

The finally clause is also executed "on the way out" when any other clause of the try statement is left via a break, continue or return statement.

pieces = input("How many pieces of pie do you want? ")

try:
    percentage = 1/int(pieces)

except (NameError, ValueError, TypeError):
    print("You need to enter a number!")

except ZeroDivisionError:
    print("You cannot enter a zero!")

except:
    print("Something bad happened. No pie for you.")

else:
    print("You get", format(percentage, ".2%"), "of the pie!")

finally:
    print("Thank you for using the Pie Program!")

This outputs like this:

How many pieces of pie do you want? 2
You get 50.00% of the pie!
Thank you for using the Pie Program!
How many pieces of pie do you want? 0
You cannot enter a zero!
Thank you for using the Pie Program!
How many pieces of pie do you want? One
You need to enter a number!
Thank you for using the Pie Program!

As you can see, the finally clause is executed in any event.

In real world applications, the finally clause is useful for releasing external resources (such as files or network connections), regardless of whether the use of the resource was successful.

Launch Exercise

So here is our full, annotated try / except for the pie program:

# Get input from the user
pieces = input("How many pieces of pie do you want? ")

# Begin the try / except statement
try:
    # This is the line which might throw an exception
    percentage = 1/int(pieces)

# This will catch NameError,
# ValueError, and TypeError exceptions
except (NameError, ValueError, TypeError):
    # This displays if one of these errors are thrown
    print("You need to enter a number!")

# This will catch ZeroDivisionError errors
except ZeroDivisionError:
    # This displays if ZeroDivisionError is thrown
    print("You cannot enter a zero!")

# This catches anything else which might go wrong
except:
    print("Something bad happened. No pie for you.")

# This code runs if no exception is thrown
else:
    print("You get", format(percentage, ".2%"), "of the pie!")

# This code runs regardless of whether an exception is thrown
finally:
    print("Thank you for using the Pie Program!")

Debugging

A skill that you should cultivate as you program is always asking yourself, "What could go wrong here?" or alternatively, "What crazy thing might our user do to crash our (seemingly) perfect program?"

For example, look at the program which we used to demonstrate the while loop in the chapter on iteration:

while True:
    line = input('Enter something: ')
if line[0] == '#' :
    continue
if line == 'done':
    break
print(line)
print('Done!')

Look what happens when the user enters an empty line of input:

Enter something: Hello!
Hello!
Enter something: # Don't print thisEnter something:

Traceback (most recent call last):
  File "examples.py", line 3, in <module>
    if line[0] == '#' :
IndexError: string index out of range

The code works fine until it is presented an empty line. Then there is no zero-th character, so we get a traceback. There are two solutions to this to make line three "safe" even if the line is empty.

One possibility is to simply use the startswith method which returns False if the string is empty.

if line.startswith('#') :

Another way is to safely write the if statement using the guardian pattern and make sure the second logical expression is evaluated only where there is at least one character in the string.:

if len(line) > 0 and line[0] == '#' :

Finally, if we are not sure what all could go wrong, the program could be placed within a try / except statement:

while True:
    line = input('Enter something: ')
    try:
        if line[0] == '#' :
            continue
    except:
        line = input('Enter something: ')
        
    else:
        if line == 'done':
            break
        print(line)
print('Done!')