More Exceptions
Raising
All those exit lines are getting a little tedious. Our quadratic function should probably be handling some of these errors. Let’s change our quadratic function to handle some of these errors:
def quadratic(a, b, c):
if a == 0:
raise ValueError('Variable "a" cannot be 0')
x1 = -1 * b / (2 * a)
try:
x2 = math.sqrt(b ** 2 - 4 * a * c) / (2 * a)
except ValueError:
raise ValueError("Cannot take square root of negative number")
return (x1 + x2), (x1 - x2)
def main(args):
try:
a, b, c = (float(x) for x in args)
except ValueError:
print("Error: Three numeric arguments required")
exit(1)
solution1, solution2 = quadratic(a, b, c)
print(f"x = {solution1} or {solution2}")
Now when we run our program, we’ll see two different types of exceptions raised. The first one just raises a ValueError:
$ python quadratic.py 0 0 0
Traceback (most recent call last):
File "quadratic.py", line 27, in <module>
main(sys.argv[1:])
File "quadratic.py", line 22, in main
solution1, solution2 = quadratic(a, b, c)
File "quadratic.py", line 7, in quadratic
raise ValueError('Variable "a" cannot be 0')
ValueError: Variable "a" cannot be 0
But the second one tells us that an exception was raised while raising another exception:
$ python quadratic.py 4 1 1
Traceback (most recent call last):
File "quadratic.py", line 10, in quadratic
x2 = math.sqrt(b ** 2 - 4 * a * c) / (2 * a)
ValueError: math domain error
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "quadratic.py", line 27, in <module>
main(sys.argv[1:])
File "quadratic.py", line 22, in main
solution1, solution2 = quadratic(a, b, c)
File "quadratic.py", line 12, in quadratic
raise ValueError("Cannot take square root of negative number")
ValueError: Cannot take square root of negative number
This is a feature added in Python 3. This kind of thing often occurs when your exception handling code has an exception.
We actually meant to raise this second exception, so tell Python we’re re-raising the exception:
def quadratic(a, b, c):
if a == 0:
raise ValueError('Variable "a" cannot be 0')
x1 = -1 * b / (2 * a)
try:
x2 = math.sqrt(b ** 2 - 4 * a * c) / (2 * a)
except ValueError as error
raise ValueError("Cannot take square root of negative number") from error
return (x1 + x2), (x1 - x2)
That error is a variable name that represents the actual exception instance. We are telling Python that we are raising a new exception that is a replacement for our original exception.
Now Python knows that we meant to bubble this exception up using a new exception:
$ python quadratic.py 4 1 1
Traceback (most recent call last):
File "quadratic.py", line 10, in quadratic
x2 = math.sqrt(b ** 2 - 4 * a * c) / (2 * a)
ValueError: math domain error
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "quadratic.py", line 27, in <module>
main(sys.argv[1:])
File "quadratic.py", line 22, in main
solution1, solution2 = quadratic(a, b, c)
File "quadratic.py", line 12, in quadratic
raise ValueError("Cannot take square root of negative number") from exc
ValueError: Cannot take square root of negative number
This raise-from syntax is a Python 3 only feature.
Users don’t like to see exceptions. They’re not pretty. Let’s refactor our code to make our own exception type that will stop the program, but which we can catch to show a useful error message.
import math
import sys
def quadratic(a, b, c):
if a == 0:
raise QuadraticError('Variable "a" cannot be 0')
x1 = -1 * b / (2 * a)
try:
x2 = math.sqrt(b ** 2 - 4 * a * c) / (2 * a)
except ValueError as error:
raise QuadraticError(
"Cannot take square root of negative number") from error
return (x1 + x2), (x1 - x2)
def main(args):
try:
a, b, c = (float(x) for x in args)
except ValueError:
raise QuadraticError("Three numeric arguments required")
solution1, solution2 = quadratic(a, b, c)
print(f"x = {solution1} or {solution2}")
class QuadraticError(ValueError):
"""Error raised when incorrect quadratic arguments are provided."""
if __name__ == "__main__":
try:
main(sys.argv[1:])
except QuadraticError as error:
print(error)
exit(1)
$ python quadratic.py
Three numeric arguments required
$ python quadratic.py 4 1 1
Cannot take square root of negative number
$ python quadratic.py 0 0 0
Variable "a" cannot be 0
$ python quadratic.py 1 5 1
x = -0.20871215252208009 or -4.7912878474779195
We will talk about classes and inheritance a bit later in the course, but what we have done is make our very own error type, QuadraticError, which inherits from the ValueError type.
It will be caught by any try-except block that has an except for either QuadraticError, ValueError or any exception types above them in the exception hierarchy.
>>> from quadratic import quadratic
>>> try:
... quadratic(0, 2, 3)
... except ValueError as error:
... print("A ValueError was raised:", error)
... print(type(error))
...
A ValueError was raised: Variable "a" cannot be 0
<class 'quadratic.QuadraticError'>
If a different kind of error is raised, it is not caught by our except block:
>>> try:
... quadratic(4, 2, 'a')
... except ValueError as error:
... print("A ValueError was raised:", error)
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
File "/Users/diane/python_class/quadratic.py", line 10, in quadratic
x2 = math.sqrt(b ** 2 - 4 * a * c) / (2 * a)
TypeError: unsupported operand type(s) for -: 'int' and 'str'
The Finally Block
Python provides a finally block for the situation where we need to do something at the end of a try-except, regardless of whether there was an error caught or not. The finally block will always be executed after all the other blocks belonging to the try.
Modifying our quadratic.py program, we could add a finally block to make sure we always say Goodbye before exiting:
if __name__ == "__main__":
try:
main(sys.argv[1:])
except QuadraticError as error:
print(error)
exit(1)
finally:
print("Goodbye!")
The finally block will be executed after everything else from the try statement has finished:
$ python quadratic.py 0 4 8
Variable "a" cannot be 0
Goodbye!
$ python quadratic.py 4 1 1
Cannot take square root of negative number
Goodbye!
$ python quadratic.py 3 4 1
x = -0.3333333333333333 or -1.0
Goodbye!
$ python quadratic.py
Three numeric arguments required
Goodbye!
Bare Except Clause
So far, we have always seen a non-empty except clause of our programs. You can actually leave the except clause blank, but you should never do this. I’m going to show you why not.
Let’s make a simple program, favorite.py, that repeatedly asks the user for input.
We’re going to use two new constructs in this program:
a
whileloop, which is a loop that keeps going until a given condition is falsethe built-in
inputfunction which allows us to prompt the user for input
Note
Python 2 has two types of input functions, input and raw_input. raw_input returns a string, and input tries to run the input as a Python expression. Recognizing that you’re most likely to want a string when asking for input, Python 3 changed the default behavior of input to return a string.
while True:
number = int(input("What is your favorite number? "))
print(f"{number} is a good number")
If we try to provide a non-numeric input to this program we’ll see an exception. Let’s catch all exceptions like this:
while True:
try:
number = int(input("What is your favorite number? "))
print(f"{number} is a good number")
except:
print("Uh oh. That's not a number. Try again!")
We should be able to press Ctrl-C to exit our program. Unfortunately we cannot. There is no way to close our program!
If we explicitly catch Exception, this problem will be resolved:
while True:
try:
number = int(input("What is your favorite number? "))
print(f"{number} is a good number")
except Exception:
print("Uh oh. That's not a number. Try again!")
Now when we run our program Ctrl-C actually exits.
So what’s going on here is that all exceptions inherit from BaseException but system-exiting exceptions do not inherit from Exception. A bare except clause is the same as catching BaseException.
A hierarchy of the built-in exception classes can be found in the Python documentation.
Exception Types
By catching Exception, we’re catching all non-system-exiting exceptions:
while True:
try:
number = int(input("What is your favorite number? "))
print(f"{number} is a good number")
except Exception:
print("Uh oh. That's not a number. Try again!")
We could improve this code by catching only relevant exception types:
while True:
try:
number = int(input("What is your favorite number? "))
print(f"{number} is a good number")
except ValueError:
print("Uh oh. That's not a number. Try again!")
We know that int will raise a ValueError if we give it an invalid string. If some other type of exception was raised, that’s something we didn’t count on and it could even be a programming bug. We don’t want to blame the user for our error. Our error message is only relevant for the specific error that we’re counting on.
The lessons today include links to explanations for various types of built-in exceptions and their meanings.
Clean Up
We could also have improved the infinite looping code we just wrote by using an else block in our try-except:
while True:
try:
number = int(input("What is your favorite number? "))
except ValueError:
print("Uh oh. That's not a number. Try again!")
else:
print(f"{number} is a good number")
When catching an exception, the only thing that should go in the try block is code which we’re trying to catch an exception on. Before we added this else block, if a ValueError were raised in our print statement somehow, we would have caught that too.
You should always write your try-except blocks to be as narrow as possible. This way you will only catch the types of exceptions you’re expecting and only catch exceptions in the lines of code you’re expecting. Any unexpected exceptions will keep bubbling up the stack this way.