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.
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.
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:
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.
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.
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.
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!')