Exceptions
Programs that use external data can fail because a file is missing, a webpage changed, or a line of input is invalid. Python represents these failures as exceptions. A traceback identifies an exception, while try and except let a program handle expected failures.
Reading a traceback
When Python cannot finish a line, it stops the program and prints a traceback that reports what went wrong and where. Dividing by zero produces a short one:
>>> 1 / 0
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ZeroDivisionError: division by zeroRead a traceback from the bottom up, starting with the last line. ZeroDivisionError is the type of the exception, and division by zero is the message spelling out what happened. The indented lines above it are the call stack, naming the file and line number where the failure occurred. Here that is line 1 of the interactive prompt. In a program, it identifies the file and line where the exception occurred. An unhandled exception ends the program, so every line after the failing one is skipped. You can control this behavior with try and except.
Common exceptions
The traceback names the exception type, and these common types point to different causes:
| Exception | When it happens |
|---|---|
FileNotFoundError |
opening a file that isn’t there: open('missing.txt') |
KeyError |
asking a dictionary for a key it doesn’t have: tweet['created_at'] |
IndexError |
indexing a list past its end: xs[10] on a three-item list |
TypeError |
combining incompatible types: 'age: ' + 35, or len(5) |
ValueError |
a value of the right type but the wrong content: int('2.3') |
NameError |
using a variable that was never defined, usually a typo: usernme |
AttributeError |
calling a method a value doesn’t have: 'hello'.append('!') |
ZeroDivisionError |
dividing by zero: 1 / 0 |
AssertionError |
an assert whose condition turns out false |
A tenth, UnboundLocalError, is a kind of NameError that occurs inside functions. You do not have to memorize the table; read the type named by the traceback and connect it to its likely cause. A KeyError, for example, points to a missing dictionary key. This week’s practice quiz tests that identification and comes with answers produced by running each snippet. Investigate any result that differs from your answer. More worked examples appear in chapter 11 of Automate the Boring Stuff and the Python docs on built-in exceptions.
Handling an error with try and except
Some failures are unexpected and should stop the program so you can fix the code. Others are foreseeable and can be handled, such as a missing settings file for a first-time user. There are two ways to write that, and Python programmers have names for both.
The first is to ask permission: check that everything is in order before you act. We can test whether a file exists with os.path.exists before opening it:
import os
if os.path.exists('scores.txt'):
with open('scores.txt') as f:
print(f.read())
else:
print('could not find scores.txt')This approach requires a separate check for every failure you want to predict. The second way is to ask forgiveness: just try the thing, and deal with the failure if it comes. That is what try and except are for.
The unhandled failure looks like this:
>>> open('scores.txt')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
FileNotFoundError: [Errno 2] No such file or directory: 'scores.txt'Wrap the risky line in a try block and name the expected exception in an except block:
try:
with open('scores.txt') as f:
text = f.read()
print(text)
except FileNotFoundError:
print('could not find scores.txt')Save that as read_scores.py and run it with the file still missing:
$ python3 read_scores.py
could not find scores.txtIf a FileNotFoundError occurs inside the try block, Python stops that block and runs the except block. If the file exists and no error is raised, Python skips the except block and prints the text. The program continues in either case.
A try block can stop in the middle
A try block stops at the first exception and skips its remaining lines. Print statements around a failing line show the order:
try:
print('opening the file')
f = open('scores.txt')
print('file opened')
text = f.read()
print('done')
except FileNotFoundError:
print('could not find scores.txt')Run it with no scores.txt present and it prints only two lines:
$ python3 partial.py
opening the file
could not find scores.txtThe open call fails, so file opened, the read, and done never happen; Python leaves the block at the point of failure and goes straight to except. If several steps must happen together, an exception may leave some complete and others untouched, so keep only related operations inside one try.
Catch the exception you mean
A bare except catches every exception and can hide unrelated bugs:
try:
text = open('scores.txt').read()
except:
print('could not find scores.txt')If you misspell open as opne, it catches the resulting NameError and incorrectly prints could not find scores.txt. Naming the expected exception lets other errors still produce tracebacks while you write the code. When a block can raise more than one error you mean to handle, list them together: except (FileNotFoundError, PermissionError):.
Surviving messy data
Later programs in this course process many files or webpage records. Put the try inside the loop so that an invalid item skips one iteration instead of stopping the program:
numbers = ['10', '20', 'oops', '40']
total = 0
for n in numbers:
try:
total = total + int(n)
except ValueError:
print('skipping bad value:', n)
print('total:', total)Its output is:
$ python3 total.py
skipping bad value: oops
total: 70int('oops') raises a ValueError, the except catches it, we note the skip, and the loop moves on to '40'. If the try were outside the loop, the first bad value would stop the entire calculation. Inside the loop, the program skips only that value.
Web scrapers use this same loop over data from other sites. A scraped item is usually a dictionary, and dictionaries raise KeyError when a field you expected is missing:
>>> tweet = {'text': 'hello world'}
>>> tweet['created_at']
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'created_at'A missing created_at field can crash a scraper’s loop unless the field access is inside a try that skips items without it. The web scraping project uses this pattern when processing many items. This week’s password-cracking lab uses it to try a password, catch the failure, and move on to the next one.
Raising your own exceptions
A function can deliberately raise an exception with an explanatory message when it receives input it cannot process:
def average(numbers):
if len(numbers) == 0:
raise ValueError('cannot average an empty list')
return sum(numbers) / len(numbers)average checks for empty input and raises a ValueError with a specific message instead of allowing division to raise ZeroDivisionError. The caller can wrap average in a try and handle the bad case by name:
try:
result = average([])
except ValueError as e:
print('problem:', e)The as e clause assigns the exception object to e so we can print its message:
$ python3 average.py
problem: cannot average an empty listassert checks a condition you believe must hold and raises an AssertionError if it does not. For example, assert len(numbers) > 0 is a one-line guard against an empty list.
When you are stuck
Many errors can be solved by reading the exception type and line number in the traceback. For harder errors, use these debugging techniques:
Explain the broken code out loud, line by line, to a rubber duck or a patient friend; you may hear your own mistake before you finish. Add a print to inspect a variable’s value, and read the documentation for the function that is misbehaving. Re-running unchanged code provides no new information.
Practice
Cowsay practices environment setup, package installation with pip, and code cleanup with a linter, all of which later projects require. Password cracking uses a try/except loop to test thousands of candidate passwords and catch each failure until one works.