Well, we've spent the majority of the book so far learning how to program, and we've broken a bunch of things along the way. Since we've gotten into so much trouble by trying to expose the error systems in Python, maybe it's time to learn how to handle the errors that we keep bringing up.
In this chapter, we'll learn about error handling--the proper way to catch exceptions when they arise, and how to write code that can recover from what would otherwise have been a fatal error.
By this point, you've probably run into a few exceptions along the way. At least, I hope you have! We're setting out to break as many things as we can in the name of learning, after all!
Exceptions can happen when you try to convert a string into a number when the string is actually a person's name, or when you misspell a function or variable name forcing Python to complain that it can't find the thing that you're trying to call. Exceptions are Python's way of identifying where and how a part of your program broke. Once you're able to identify what they mean, you can use these to help fix your code.
It's probably safe to assume that you've hit a red warning message at some point in your programming. It might have been clear what happened, but it also might have been a total mystery. Some of the error messages can be confusing. Only with experience are you able to make the most sense of them. Other exception messages sound perfectly reasonable, and you might even find yourself understanding how to deal with them in a responsible way.
Let's look at an example that you might have run into already. We'd like to get a number as input from a user so that we can do some math with it. Maybe we're squaring it, maybe we're adding two. Regardless, we'd like to get a number from the user, and to use the number value, the string value must be converted. What happens when the user enters a word, or for that matter, any non-numeric value, and the conversion is omitted?
>>> user_number = int(input("Enter a number: "))
Enter a number: Alexander
Traceback (most recent call last):
File "<pyshell#2>", line 1, in <module>
user_number = int(input("Enter a number: "))
ValueError: invalid literal for int() with base 10: 'Alexander'
Python complains in bright red text about a ValueError exception, and states that the string "Alexander" is an invalid literal for int() with base 10. So what on Earth does that mean? It's pretty clear that int was given a value that can't be converted into a number. Why did Python actually throw a bright red error and terminate the program in this way? Is this really the graceful way to handle the problem?
Let's try another example to get Python to halt program execution with another exception. Consider the case where we get an acceptable number, but a number that breaks the mathematical operation we'd like to use.
>>> user_number = int(input("Enter a number: "))
Enter a number: 0
>>> print("10 divided by {0} is {1}.".format(user_number, 10/user_number))
Traceback (most recent call last):
File "<pyshell#4>", line 1, in <module>
print("10 divided by {0} is {1}.".format(user_number, 10/user_number))
ZeroDivisionError: int division or modulo by zero
This sample code broke because we tried to divide 10 by 0, which is undefined. Python threw the appropriately-named ZeroDivisionError to indicate that this was the specific problem.
An exception is Python's way to break the code in a reasonable and safe way that can actually be identified by your program. For example, if it is known that a division operation could potentially attempt to divide by zero causing a ZeroDivisionError, we could tell the code to watch for this error message, and to handle it gracefully if it happens. In the ValueError example, a loop could be repeated as long as ValueError exceptions came back from int, since a ValueError exception means that the user is trying to enter in values that aren't accepted by the int function.
Fortunately, there is functionality to attempt to run a program with the knowledge that some part of the code might actually throw an exception. It's like saying that you know you're asking the user to enter in a value, but you also know that they're capable of giving you bad data. You'd like to try to run this block of code, and if an exception comes back, you'd like to handle it in an appropriate way.
In the ValueError example, let's make a very simple piece of code that attempts to convert the input to an integer, and to catch a ValueError if the user gives inappropriate data.
try:
user_number = int(input("Enter a number: "))
print("Your number is {0}.".format(user_number))
except ValueError as e:
print("You didn't enter a number! Shame on you.")
print(e)
print("All done!")
Enter a number: 10
Your number is 10.
All done!
Enter a number: Alexander
You didn't enter a number! Shame on you.
invalid literal for int() with base 10: 'Alexander'
All done!
This is certainly much better than a program crash! If the program gets an acceptable value, everything goes as planned and the except block never gets executed. However, Python understood that there was a block of code that might throw a ValueError exception, and if it happened, it should stop immediately and execute the code that prints out a stern rebuke. Notice that it really does exit immediately. A try block is your way of asking Python to cautiously execute the code, and to get out of there as soon as possible once an Exception occurs. That's why you don't see the "Your number is" string when the bad value is given. Python gets out as soon as the int statement occurs.
If you want to access detailed information about the specifics of the crash, you can add "as e", or "as (variable_name)" to the except statement. That gives you the ability to have a statement like print(e) that gives more information about the details of the exception being raised. In future examples, we generally won't need to deal with those details, and the "as (variable_name)" component will be safely omitted.
Once the try block and corresponding except block are finished, the program continues on as it normally would. There really is no crash here when an exception is raised and caught. Python catches the exception, does something with it (if you'd like to do something at all), and then moves forward as if everything was fine. Presumably you would use the except block to make sure that your program state was in a good place moving forward.
What about the division case? If for some reason the division needs to occur, we don't want to move forward without a valid number. Let's write a loop.
A try block can occur inside of a loop, and we can use try blocks to carefully identify only the pieces of code that are potential candidates for exceptions to occur. Let's rewrite the ZeroDivisionError code to loop until a valid number occurs, and to do the division once everything is finished and we are sure that a good number has been placed in user_number. First, here is the naive case that tries once to get a valid number.
try:
user_number = int(input("Enter a number other than zero: "))
division = 10 / user_number
print("10 divided by {0} is {1}.".format(user_number, 10 / user_number))
except ZeroDivisionError:
print("A number other than zero, please!")
From this, let's move the try block inside of a loop.
done = False
while not done:
user_number = int(input("Enter a number other than zero: "))
try:
division = 10 / user_number
done = True
except ZeroDivisionError:
print("A number other than zero, please!")
print("10 divided by {0} is {1}.".format(user_number, 10 / user_number))
Enter a number other than zero: 0
A number other than zero, please!
Enter a number other than zero: 0
A number other than zero, please!
Enter a number other than zero: 25
10 divided by 25 is 0.4.
With the try-block nicely nested inside the while loop, we get away from the messy example of wrapping the entire block in a try section. It really is only the division operation that can cause this problem, so that single line is wrapped in a try block along with the only modification to done that can terminate the while loop. If the division raised a ZeroDivisionError, the statement that sets done to True never gets hit because Python swiftly shifts the program execution into the except block.
A keen observer will have noticed that we have the exact same ValueError problem in this new code. There's nothing to stop a malicious user from entering a word in the input block, and the try statement in the code above won't catch bogus strings that can't be converted into integers.
Try blocks can have multiple except conditions, just like if-statements can have multiple elif blocks followed by an else block. If the input statement is moved inside the try block, and another except statement is introduced to catch ValueError exceptions, the code might look like this:
done = False
while not done:
try:
user_number = int(input("Enter a number other than zero: "))
division = 10 / user_number
done = True
except ValueError:
print("You didn't enter a number! Shame on you.")
except ZeroDivisionError:
print("A number other than zero, please!")
print("10 divided by {0} is {1}.".format(user_number, 10 / user_number))
Enter a number other than zero: Alexander
You didn't enter a number! Shame on you.
Enter a number other than zero: 0
A number other than zero, please!
Enter a number other than zero: 25
10 divided by 25 is 0.4.
This code nicely merges both of the examples seen so far. The code won't crash on a non-numeric string, and it also won't die when a zero value is given. It sits inside of a loop that repeats until a valid condition is obtained, and finally moves on to the goal state where we get to print out the result of the division.
You are not a slave to exceptions! You don't need to sit idly by waiting to catch them at their fancy. You have the ability to raise exceptions yourself when your code breaks in a unique way.
To start, we can write a simple loop that duplicates the modulo (mod) operator--a way to determine the remainder of devision of one number by another. For example, if we evaluate the expression 6 mod 4, the result is 2, because 4 goes into 6 one time and leaves a remainder of 2. In Python, we can use the % operator to find the remainder.
>>> 6 % 4
2
Now, in the interest of learning something new, and in potentially breaking something in a creative way, let's build a loop that will duplicate this functionality. First, the naive solution looks like this:
x = int(input("Enter the first number: "))
y = int(input("Enter the second number: "))
print("{0} mod {1} is {2}".format(x, y, x % y))
Enter the first number: 6
Enter the second number: 4
6 mod 4 is 2
We can replace the explicit x % y operation in the print statement with a reference to a variable we create to store the result. Let's try this:
x = int(input("Enter the first number: "))
y = int(input("Enter the second number: "))
m = x
while True:
m = m - y
if m < y:
break
print("{0} mod {1} is {2}".format(x, y, m))
Enter the first number: 6
Enter the second number: 4
6 mod 4 is 2
In this example, we replace the modulo operator with a loop. Before the loop begins, we create a new variable called m. In m, we store the value of x, and iteratively subtract y from m until we're left with a value that is less than our divisor. In the case of 6 mod 4, subtracting 4 from 6 once gives us 2, which can't bear another subtraction of the divisor before heading into the negative values. We halt the loop when this condition is met, and in m, the correct value is stored.
So what's wrong with this code? Well, a number of things. We wouldn't be breaking stuff otherwise.
First, consider what it means to divide by zero. How many times does zero go into another number? The result of dividing a number of zero is undefined--there's no result that satisfies the expression. We can see this clearly in Python through an exception.
>>> 6 / 0
Traceback (most recent call last):
File ">stdin<", line 1, in <module>
ZeroDivisionError: integer division or modulo by zero
The program halts, as expected, with the appropriately named ZeroDivisionError exception. Do you see that bit at the end of the expression detail though where it suggests that this is a result of either an integer division by zero or a modulo by zero? What happens if we use the modulo operator with zero?
>>> 6 % 0
Traceback (most recent call last):
File ">stdin<", line 1, in <module>
ZeroDivisionError: integer division or modulo by zero
Ouch! Same thing. There can't be a remainder after division because we can't even divide by zero in the first place. So what does that mean for our new program?
Enter the first number: 6
Enter the second number: 0
^CTraceback (most recent call last):
File "sample.py", line 5, in <module>
while True:
KeyboardInterrupt
The program doesn't end! It repeats in an infinite loop since the terminating condition of m < y can never be true. Subtract zero from m as many times as you want, but m can never be less than y. We're not defeated though. This is just another way to break the code and hint at the solution.
In the Python implementation of modulo, the correct response is to give the ZeroDivisionError exception when zero is provided as the divisor. We can do the same thing in our code.
Throwing an exception is done using the raise keyword. By raising an exception, you halt the code at the current point and exit out to a point at which the exception can be caught, or the program can be terminated.
>>> raise Exception("Oh no!")
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
Exception: Oh no!
An exception is a type of class, which we'll talk about in more detail later. For our purposes, though, we can use exceptions that we know to exist already in our code, especially if we're writing similar blocks of code to fragments that exist in Python. Let's try adding a raised exception to our sample modulo.
x = int(input("Enter the first number: "))
y = int(input("Enter the second number: "))
if y == 0:
raise ZeroDivisionError("modulo by zero")
m = x
while True:
m = m - y
if m < y:
break
print("{0} mod {1} is {2}".format(x, y, m))
Enter the first number: 6
Enter the second number: 0
Traceback (most recent call last):
File "sample.py", line 5, in <module>
raise ZeroDivisionError("modulo by zero")
ZeroDivisionError: modulo by zero
Beautiful. We even get our customized message that omits the integer error possibility. You can write whatever you'd like in there.
And hey, by the way, if you'd like to break some stuff, try adding in negative numbers. This code isn't perfect yet!
Code can break in lots of ways. In fact, in many cases, code breaks by design. To make sure that our code runs as expected, even in the situation where we're given data that is clearly malformed or inappropriate for our purposes, throwing exceptions is the Pythonic way to handle those errors and to continue or die gracefully.
In the next section, we will introduce the concept of a function, a way to compartmentalize blocks of code that can be reused without resorting to simply copying and pasting.