Exception Handling
As you've worked through the lessons and tried out the code, you've seen plenty of Python error messages. Most of them have been syntax errors, which are caused by mistakes made while entering code examples. The rest, caused by all other kinds of mistakes, are called exceptions.
Syntax errors can and will crash your program, but soon you'll know exactly how to diagnose and fix them! Let's go right to work. Type the commands below in an interactive session as shown:
>>> print('Hello, world)
File "<stdin>", line 1
print('Hello, world)
^
SyntaxError: unterminated string literal (detected at line 1)
>>> print('Hello, world')
Hello, world
| Modern Python | The original error message was SyntaxError: EOL while scanning string literal.
Modern Python (3.12+) gives the more specific
SyntaxError: unterminated string literal (detected at line 1). |
To handle this sort of error message, correct the syntax of your code so it makes sense to the interpreter. This is the most common kind of bug, and now you know how to squash it!
But even if your code is syntactically correct, it can still throw exceptions when you run it. The most common exceptions are TypeError, KeyError, and NameError. The odds are pretty good that you've encountered them already during this course, for instance if you mistyped a variable name or entered a non-numeric value that your program tried to convert into a number. Let's take a look at something like that. Type the commands below as shown:
>>> 'chapter ' + 15
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
'chapter ' + 15
~~~~~~~~~~~^~~~
TypeError: can only concatenate str (not "int") to str
>>> snakes = {'python':'fun','mamba':'dance'}
>>> snakes['cobra']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
snakes['cobra']
~~~~~~^^^^^^^^^
KeyError: 'cobra'
>>> print(my_var)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
print(my_var)
^^^^^^
NameError: name 'my_var' is not defined
| Modern Python | Since Python 3.11, tracebacks include a line of ~ and ^ characters
pointing precisely at the subexpression that caused the error. The original 3.1 tracebacks showed only the
offending line without this fine-grained underlining. The TypeError message also changed: the original read
cannot concatenate 'str' and 'int' objects; modern Python says
can only concatenate str (not "int") to str. |
Keep this interactive session open because we'll be doing another code example with the snakes dict.
The first lesson of exception handling is learning to catch exceptions, and then handle them so that they don't bring your program crashing down. The next level of exception handling teaches you how to handle different types of exceptions at the same time.
As you saw in the interactive session above, the interpreter raises a KeyError exception when a dict does not contain a key specified as an index for retrieval. You catch errors using try/except statements like the one in our next example. Type the commands below as shown:
>>> try:
... snakes['cobra']
... except KeyError:
... print('Exception detected')
...
Exception detected
The try statement attempts to execute the code contained in its indented suite. That suite may be made up of several lines of code, but this example attempts to evaluate only the expression snakes['cobra']. (This key was chosen intentionally because it will raise an exception). This causes the interpreter to trigger the exception handler for the KeyError exception, the except statement. The except suite contains the expression print('Exception detected'). Ideally your messages will be rather more specific about what happened.
Congratulations—you caught an exception! Of course, the exception handler does nothing for you if you don't handle the correct exception. The next example illustrates this point. Type the code below as shown:
>>> try:
... 3/0
... except KeyError:
... print("Exception detected")
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
3/0
~^~
ZeroDivisionError: division by zero
>>>
Although the try statement has an exception handler, it doesn't handle the specific exception (ZeroDivisionError) that is raised. In this case, the interpreter behaves as if there is no handler. In the interactive interpreter, this means you see a "stack traceback." If an unhandled exception happens when you are running a program, you still get the stack traceback, and then the program terminates.
In earlier lessons, you used mathematical algorithms to learn about integers, loops, and functions. In some cases though, you ran into problems verifying numeric input. A good example is the sort of input() problem shown here. Type the commands below as shown:
>>> inp = input('Integer: ')
Integer: four
>>> 10 + int(inp)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
10 + int(inp)
~~~^^^^^
ValueError: invalid literal for int() with base 10: 'four'
Exception handling combined with a loop is really handy. You write an infinite loop, and break out of it when the user's input does not raise an exception. Let's try that problem again. Type the commands as shown:
>>> while True:
... inp = input("Integer: ")
... try:
... print(int(inp)+10)
... break
... except ValueError:
... print("Please enter an integer")
...
Integer: thing
Please enter an integer
Integer: python
Please enter an integer
Integer: 12.3
Please enter an integer
Integer: 12
22
>>>
This loop won't blow up if a user enters non-numeric data for a numeric field, which is a reasonably common pattern in Python coding.
If you don't catch an exception, it will ultimately be raised to the interpreter. But if a try statement is contained inside another one, the outer try's exception handler gets the chance to handle the exception. Create a new program in the editor window as shown:
""" Nested exception handling"""
def divide(a, b):
""" Return result of dividing a by b """
print("=" * 20)
print("a: ", a, "/ b: ", b)
try:
try:
return a/b
except TypeError:
print("Invalid types for division")
except ZeroDivisionError:
print("Divide by zero")
if __name__ == "__main__":
print(divide(1, "string"))
print(divide(2, 0))
print(divide(123, 4))
Save it as nested.py and run it:
==================== a: 1 / b: string Invalid types for division None ==================== a: 2 / b: 0 Divide by zero None ==================== a: 123 / b: 4 30.75
The statement print(divide(1, "string")) raises a TypeError exception because it isn't possible to divide a number by a string. This exception is caught by the inner handler and handled. The function then ends without returning a value, so its result is None. The statement print(divide(2, 0)) also raises an exception, but in this case it isn't caught by the except of the inner try because it isn't a TypeError. Consequently, the exception "bubbles up" to the next level, where there is a handler for the ZeroDivisionError that occurs. Finally, the statement print(divide(123, 4)) represents a legal arithmetic operation and gets past both error handlers and returns the appropriate result.
By nesting exception handlers, you can catch errors that are thrown at different levels and handle them appropriately. Every additional level of nesting removes some readability from your program, though, so avoid doing it when you can. Fortunately, you can avoid some of that because Python allows you to attach several except clauses to a single try statement. Edit nested.py below as shown:
""" Nested exception handling"""
def divide(a, b):
""" Return result of dividing a by b """
print("=" * 20)
print("a: ", a, "/ b: ", b)
try:
try:
return a/b
except TypeError:
print("Invalid types for division")
result = a/b
print("Sometimes executed")
return result
except TypeError:
print("Invalid types for division")
except ZeroDivisionError:
print("Divide by zero")
if __name__ == "__main__":
print(divide(1, "string"))
print(divide(2, 0))
print(divide(123, 4))
Save and run it. When the exception is raised inside of the try suite, the interpreter tries to match it against each of the except clauses, in turn. If it finds a matching clause, it executes the associated handler suite. If none of the except clauses match the exception, then none of the handlers are run, and the interpreter starts to examine the handlers of any outer try statements. The output from running this program should look like this:
==================== a: 1 / b: string Invalid types for division None ==================== a: 2 / b: 0 Divide by zero None ==================== a: 123 / b: 4 Sometimes executed 30.75
The print("Sometimes executed") statement and the following return aren't executed when an exception is raised. One particularly useful feature of exceptions is that you can use them to change the flow of your program's logic when conditions are, well, exceptional.
Sometimes you want to take the same action for several different exceptions. You can do this by specifying the exceptions as a tuple after the except keyword. Then the handler will be executed if any of the exceptions in the tuple occur during execution of the try clause.
You may want to be able to flag error conditions from your own code. This is especially useful when you are writing code to be used by other people. You flag error conditions with the raise statement; this is useful in two contexts:
- If you want to handle some of the consequences of an exception, but then re-raise it to be handled by some outer handler, you can do so by executing a statement consisting of only the keyword raise. This will cause the same exception to be presented to the outer handlers.
- If you detect some condition in your code that requires you to stop running the program, you can raise a specific exception of your choice by following the raise keyword with an exception. You can create that exception by calling any of the system exceptions with a string argument. Let's try out some of these features in nested.py. Type the code below as shown:
""" Nested exception handling"""
def divide(a, b):
""" Return result of dividing a by b """
print("=" * 20)
print("a: ", a, "/ b: ", b)
try:
return a/b
except (ZeroDivisionError, TypeError):
print("Something went wrong!")
raise
if __name__ == "__main__":
for arg1, arg2 in ((1, "string"), (2, 0), (123, 4)):
try:
print(divide(arg1, arg2))
except Exception as msg:
print("Problem: {0}".format(msg))
Save and run it. You should see this:
==================== a: 1 / b: string Something went wrong! Problem: unsupported operand type(s) for /: 'int' and 'str' ==================== a: 2 / b: 0 Something went wrong! Problem: division by zero ==================== a: 123 / b: 4 30.75
| Modern Python | The original observe box showed Problem: int division or modulo by zero.
Modern Python's ZeroDivisionError message for integer division is simply division by zero. |
The except statement in the divide() function now specifies the same handler for both ZeroDivisionError and TypeError exceptions. The handler prints an informative message ("Something went wrong!") and then re-raises the same exception. Since there are no further handlers in the function, the re-raised exception is now caught by the except statement in the main program.
In this case, the except statement catches pretty much any exception, because all exceptions are direct or indirect subclasses of Exception. Also, the exception specification can be followed by an as clause, which specifies a name to bind to the exception that is being handled. You can see from the print() function call that when an exception is converted to a string, you get the message associated with the exception.
Using specific exceptions is handy because doing that allows you to hone in on the exact exception you want to handle. But what happens when you have code where exceptions might be raised in places you can't anticipate? Python will allow you to omit the exception specification altogether. This clause must follow all except clauses with exception specifications, and will catch any exception whatsoever. The next example uses both specific and generic specifications to catch exceptions from a Test class that possesses an add() method specifically included to produce an AttributeError or TypeError. Create a new program as shown:
""" Named and generic exception handling"""
def add(a, b):
""" Print the results of adding a set and a value"""
try:
a.add(b)
print(a)
except AttributeError:
print("({0}) is not a set object".format(a))
except TypeError:
print("({0}) is not a hashable object".format(b))
except:
print("This is a generic exception")
class Test(object):
""" Just a simple test class """
def add(self, a):
""" Demonstrates how you need to be able to handle unpredictable results. """
d = {'python':'fun'}
return d[a]
if __name__ == "__main__":
s = set((1,2,3))
add(s, 4)
add(1, 4)
add(s, [4, 5, 6])
t = Test()
add(t, 1)
Save it as exceptions.py and run it. In our add() function, we plan for 'a' to throw either an AttributeError, TypeError, or something we can't predict. Remember, the plain except clause must follow the named exceptions. In the last two lines, we attempt to use the add() method of the Test instance to use the supplied parameter as an index to a dict with only one key. Consequently, the final call to add() raises a KeyError exception, which in turn causes the final except clause to be activated, because the exception raised is neither an AttributeError nor a TypeError.
{1, 2, 3, 4}
(1) is not a set object
([4, 5, 6]) is not a hashable object
This is a generic exception
After you run the program, follow the logic through to make sure you understand exactly why it behaves the way it does.
Some Python objects are equipped with methods that remove the need for exceptions. The dict is a good example of this, as you'll see in the next code sample—the dict.get() method tries to use the first argument as a key into the dict, but if no such key exists, it returns the second argument. Type the commands below as shown:
>>> d = {1:'python'}
>>> d[1]
'python'
>>> try:
... d[10]
... except KeyError:
... print('no snake here')
...
no snake here
>>> d.get(10,'no snake here')
'no snake here'
Of course, dict.get() only works if you know that d is of type dict. If you don't know that, you might want to handle specific exceptions raised under those circumstances. For example, you might try this:
>>> d = [1,2,3,4]
>>> try:
... d[10]
... except KeyError:
... print('no snake here')
... except IndexError:
... print('no snake here either')
...
no snake here either
You need to be able to anticipate when things might go wrong with your programs, so you can catch and handle the exceptions that are raised. This will make your programs more robust, and capable of handling anything that users can throw at them.
Ideally, you build programs that never terminate with an uncaught exception. With your new knowledge of exception handling, you are much closer to reaching that goal.
You're almost there, just one lesson to go before your final project! Great work so far, keep it up!
