Exceptions¶
We have already seen exceptions. Whenever there is a problem in the code that Python can’t automatically handle, Python will raise an exception to report the error. The regular program flow is interrupted at this point.
Now we’re going to learn about what exceptions are, how to catch and deal with exceptions, and how to raise our own exceptions.
Catching Exceptions¶
Let’s make a program that allows users to solve quadratic equations.
First we’ll take a program quadratic.py that includes a quadratic function.
import math
import sys
def quadratic(a, b, c):
x1 = -1 * b / (2 * a)
x2 = math.sqrt(b ** 2 - 4 * a * c) / (2 * a)
return (x1 + x2), (x1 - x2)
def main(args):
a, b, c = (float(x) for x in args)
solution1, solution2 = quadratic(a, b, c)
print("x = {} or {}".format(solution1, solution2))
if __name__ == "__main__":
main(sys.argv[1:])
Let’s try it out:
$ python quadratic.py 2 5 3
x = -1.0 or -1.5
Let’s try calling our program without any arguments:
$ python quadratic.py
Traceback (most recent call last):
File "quadratic.py", line 18, in <module>
main(sys.argv[1:])
File "quadratic.py", line 12, in main
a, b, c = (float(x) for x in args)
ValueError: need more than 0 values to unpack
That’s not a very helpful message for users. Let’s handle that exception and make a nicer error message for the user.
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("x = {} or {}".format(solution1, solution2))
Here we introduce the “try-except” statements for handling exceptions. We have some code that might cause an error, so we put it inside of a try block. The try statement tells Python to be on the lookout for errors.
The except statement tells Python what exception to look for, or “catch”. The except block contains the code to handle the error if it occurs.
We don’t want to continue after we handle the exception, so after we print an informative message for the user, we call the system exit() function to exit Python. exit(0) means no error and any other value (we arbitrarily use 1) means there was an error. By default when a program ends normally, it is as if exit(0) was called.
If there is no exception caught, then the program flow continues after the except block.
Let’s try that out:
$ python quadratic.py
Error: Three numeric arguments required
So we can use try-except blocks to catch exceptions.
Let’s try running our program with 0 as the first argument:
$ python quadratic.py 0 1 5
Traceback (most recent call last):
File "quadratic.py", line 22, in <module>
main(sys.argv[1:])
File "quadratic.py", line 17, in main
solution1, solution2 = quadratic(a, b, c)
File "quadratic.py", line 6, in quadratic
x1 = -1 * b / (2 * a)
ZeroDivisionError: float division by zero
Uh oh. Another unhandled exception! This time the exception is raised from inside our quadratic function.
Why don’t we refactor our code to handle all exceptions?
def main(args):
try:
a, b, c = (float(x) for x in args)
solution1, solution2 = quadratic(a, b, c)
print("x = {} or {}".format(solution1, solution2))
except Exception:
print("Error: bad inputs")
exit(1)
The Exception class is the base class that all non-system-exiting exceptions inherit from.
>>> issubclass(ValueError, Exception)
True
>>> error = ValueError()
>>> isinstance(error, Exception)
True
>>> isinstance(error, ValueError)
True
Trying this new code out, we can see that this change doesn’t seem like an improvement from the user’s perspective:
$ python quadratic.py
Error: bad inputs
$ python quadratic.py 0 1 5
Error: bad inputs
The user knows even less than if we had just let the exceptions go uncaught.
Let’s refactor our code to handle each exception separately.
def main(args):
try:
a, b, c = (float(x) for x in args)
except ValueError:
print("Error: Three numeric arguments required")
exit(1)
try:
solution1, solution2 = quadratic(a, b, c)
except ZeroDivisionError:
print("Error: the first argument cannot be zero")
exit(1)
print("x = {} or {}".format(solution1, solution2))
This is much more helpful:
$ python quadratic.py
Error: Three numeric arguments required
$ python quadratic.py 0 1 5
Error: the first argument cannot be zero
Catching Multiple Exceptions¶
Can we think of another way to break this? What if we took the square root of a negative number?
$ python quadratic.py 4 1 1
Traceback (most recent call last):
File "quadratic.py", line 26, in <module>
main(sys.argv[1:])
File "quadratic.py", line 18, in main
solution1, solution2 = quadratic(a, b, c)
File "quadratic.py", line 7, in quadratic
x2 = math.sqrt(b ** 2 - 4 * a * c) / (2 * a)
ValueError: math domain error
Another error in our quadratic function call!
There are a couple of ways we can handle this. We could catch both errors at once:
def main(args):
try:
a, b, c = (float(x) for x in args)
except ValueError:
print("Error: Three numeric arguments required")
exit(1)
try:
solution1, solution2 = quadratic(a, b, c)
except (ValueError, ZeroDivisionError):
print("Error: math error")
exit(1)
print("x = {} or {}".format(solution1, solution2))
But now we’re back to giving our user very little information about what happened. A better way is to have separate except blocks for each error:
def main(args):
try:
a, b, c = (float(x) for x in args)
except ValueError:
print("Error: Three numeric arguments required")
exit(1)
try:
solution1, solution2 = quadratic(a, b, c)
except ZeroDivisionError:
print("Error: the first argument cannot be zero")
exit(1)
except ValueError:
print("Error: invalid arguments")
exit(1)
print("x = {} or {}".format(solution1, solution2))
Now we’ll get two different error messages:
$ python quadratic.py 0 0 0
Error: the first argument cannot be zero
$ python quadratic.py 4 1 1
Error: invalid arguments
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("x = {} or {}".format(solution1, 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("x = {} or {}".format(solution1, 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
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 false - the built-in
inputfunction which allows us to prompt the user for input
while True:
number = int(input("What is your favorite number? "))
print("{} is a good number".format(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("{} is a good number".format(int(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("{} is a good number".format(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("{} is a good number".format(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("{} is a good number".format(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("{} is a good number".format(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.
The Finally Block¶
Python provides a finally block for the situation where we need to do something at the end of a try-except, 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!
Exception Exercises¶
Length or None¶
Write a function len_or_none that returns the length of a given object or None if the object has no length.
>>> len_or_none("hello")
5
>>> len(4)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: object of type 'int' has no len()
>>> len_or_none(4)
>>> print(len_or_none(4))
None
>>> len_or_none([])
0
>>> len_or_none(zip([1, 2], [3, 4]))
>>> print(len_or_none(zip([1, 2], [3, 4])))
None
>>> len_or_none(range(10))
10
Average¶
Make a program average.py that calculates the average of all provided command-line arguments and prints an error message if no arguments are provided and second error message if invalid arguments are provided.
$ python average.py
No numbers to average!
$ python average.py 2 3 4 5 6 7
Average is 4.5
$ python average.py 2 3 4
Average is 3.0
$ python average.py 2 s 3
Invalid values entered, only numbers allowed!
Deep Add¶
Write a function deep_add that sums up all values given to it, including summing up the values of any contained collections.
>>> deep_add([1, 2, 3, 4])
10
>>> deep_add([(1, 2), [3, {4, 5}]])
15
Deep Flatten¶
Write a function deep_flatten that flattens all items given to it, including any contained collections, returning a list of all elements. As a bonus, make it work for strings, too.
>>> deep_flatten([1, 2, 3, 4])
[1, 2, 3, 4]
>>> deep_flatten([[1, 2], [3, [4, 5]]])
[1, 2, 3, 4, 5]
Bonus version should work like this:
>>> deep_flatten(["Hello ", "World"])
['Hello ', 'World']
>>> deep_flatten([(1, 2), [3, "bye", {4, 5}]])
[1, 2, 3, 'bye', 4, 5]
Exit Twice¶
Write a program countdown.py that counts down from a given number, pausing for 1 seconds in between each number. The program should only exit if Ctrl-C is pressed twice.
You can pause for 1 second by using sleep from the time module.
Ctrl-C pressed just once:
$ python countdown.py 5
5
4
^C Press Ctrl-C again to exit
3
2
1
Ctrl-C pressed twice:
$ python countdown.py 10
10
9
^C Press Ctrl-C again to exit
8
7
^C Goodbye!