Exceptions
After game night, the scores are saved in scores.txt. We want a report with the total and average, using the file-reading pattern from the previous reading. Start with valid scores, then test the report with a missing file, a mistyped score, and an empty file.
Reading Scores
Create scores.txt with one score per line:
10
20
40
Save this as read_scores.py next to scores.txt and run it from that folder:
numbers = []
with open('scores.txt', encoding='utf-8') as f:
for line in f:
numbers.append(int(line.strip()))
print('total:', sum(numbers))int converts each line to an integer, and sum adds the scores:
$ python3 read_scores.py
total: 70Reading a Traceback
The report works on our sample, but another organizer might move or rename the input. Rename scores.txt to scores-backup.txt and run the same program again. Python raises an exception when it cannot open the file. The traceback shows where the program failed:
Traceback (most recent call last):
File "read_scores.py", line 2, in <module>
with open('scores.txt', encoding='utf-8') as f:
FileNotFoundError: [Errno 2] No such file or directory: 'scores.txt'
Read the last line first: FileNotFoundError names the exception, followed by a message explaining it. The entry above points to line 2, where open failed. When functions call other functions, the traceback lists those calls too, with the failing operation at the bottom. The program stops here without printing a total.
Handling a Missing File
A person running the report needs to know which file to restore. We can catch FileNotFoundError and print a shorter message. Keep scores.txt missing and replace read_scores.py with this version:
try:
print('opening the file')
with open('scores.txt', encoding='utf-8') as f:
text = f.read()
print('file opened')
except FileNotFoundError:
print('could not find scores.txt')
print('finished the attempt')Python tries the code in the try block. If it raises FileNotFoundError, Python runs the matching except block:
$ python3 read_scores.py
opening the file
could not find scores.txt
finished the attemptAfter open fails, Python skips the rest of the try block, runs the handler, and continues at print('finished the attempt'). It does not retry the failed operation. Restore scores.txt and run again: the file opens, so Python skips the handler and prints file opened instead.
Skipping Invalid Scores
With the file restored, the next problem is a score someone mistyped. We want to report that row while keeping the valid scores that follow it. Now put an invalid score in scores.txt:
10
20
oops
40
int('oops') raises ValueError. Replace read_scores.py with the next version. To skip that line and still read the 40 after it, catch the exception inside the loop:
numbers = []
with open('scores.txt', encoding='utf-8') as f:
for line in f:
text = line.strip()
try:
numbers.append(int(text))
except ValueError:
print('skipping bad value:', text)
print('scores:', numbers)
print('total:', sum(numbers))Run it with the four-line file above:
$ python3 read_scores.py
skipping bad value: oops
scores: [10, 20, 40]
total: 70The bad line adds nothing to numbers. The loop continues with the next line. If try surrounded the whole loop, the first bad value would end the loop and the report would miss 40.
Raising an Exception
After skipping invalid rows, we may have no scores left. An empty list has no average: dividing its total by its length would divide by zero. Use raise to report that problem explicitly. Leave read_scores.py open and save this small test separately as average.py:
def average(numbers):
if len(numbers) == 0:
raise ValueError('cannot average an empty list')
return sum(numbers) / len(numbers)
try:
print('average:', average([]))
except ValueError as e:
print('problem:', e)The caller handles the exception raised inside average:
$ python3 average.py
problem: cannot average an empty listraise stops the function before return. The caller catches the exception with except ValueError as e. e holds the exception so we can print its message.
Complete Score Report
Return to read_scores.py and replace it with both functions and the missing-file handler. Use the file containing 10, 20, oops, and 40:
def read_scores(filename):
numbers = []
with open(filename, encoding='utf-8') as f:
for line in f:
text = line.strip()
try:
numbers.append(int(text))
except ValueError:
print('skipping bad value:', text)
return numbers
def average(numbers):
if len(numbers) == 0:
raise ValueError('cannot average an empty list')
return sum(numbers) / len(numbers)
try:
numbers = read_scores('scores.txt')
result = average(numbers)
print('scores:', numbers)
print('total:', sum(numbers))
print('average:', result)
except FileNotFoundError:
print('could not find scores.txt')
except ValueError as e:
print('problem:', e)With the mixed file, it reports the usable scores:
$ python3 read_scores.py
skipping bad value: oops
scores: [10, 20, 40]
total: 70
average: 23.333333333333332The handler inside read_scores skips individual bad scores. The handlers around the function calls catch a missing file or an empty list. Only the matching handler runs. Try an empty file, a missing file, and a file containing only oops. These checks play the same role as the doctests from the Python reading: they record the cases our program must handle.
Catching Specific Exceptions
Recovering from messy input is useful, but our own mistakes still need to be visible so we can repair them. If we mistype open as opne, Python raises NameError. A bare except: would catch that bug too, possibly printing the misleading message could not find scores.txt. Catching FileNotFoundError lets the typo produce a traceback instead.
Use pass in a handler when you deliberately want to ignore an exception and continue. It does not retry the failed line.
A Missing Return
Not every error points to the original mistake. Delete return numbers from read_scores and run the report with valid scores. The error occurs later, inside average:
TypeError: object of type 'NoneType' has no len()
Without a return, read_scores returns None, so that is what reaches len(numbers). Printing numbers just before calling average helps find the cause. Restore the return statement to fix it.
Common Exceptions
Use this guide after reading the final line of the traceback. The name of an exception tells you what kind of failure to look for:
| Exception | Example cause |
|---|---|
FileNotFoundError |
open('missing.txt') when the file does not exist |
ValueError |
int('oops'): the string does not represent an integer |
TypeError |
len(10) or 'score: ' + 10: the operation does not support that type |
NameError |
opne('scores.txt'): the name is not defined |
AttributeError |
'10'.append('20'): strings have no append method |
IndexError |
numbers[0] when numbers is empty |
KeyError |
scores['Sam'] when the dictionary has no 'Sam' key |
ZeroDivisionError |
10 / 0, 10 // 0, or 10 % 0 |
AssertionError |
assert len(numbers) > 0 when numbers is empty |
ValueError and TypeError describe different problems: int('2.3') accepts the string type but cannot interpret that text as an integer. int(2.3) works and returns 2, discarding the fractional part.
Indexes, slices, and methods
For numbers = [10, 20, 40], numbers[-1] gives 40, but numbers[-4] raises IndexError. Slices tolerate bounds beyond the end: numbers[:10] gives the whole list. A string index such as numbers['0'] raises TypeError.
Methods belong to particular types. The report uses strip on a string and append on a list. Swapping them raises AttributeError.
Function names and arguments
Assigning a number to average replaces the function stored under that name:
>>> average = 23.3
>>> average([10, 20, 40])
Traceback (most recent call last):
...
TypeError: 'float' object is not callableKeep the function name and store its result in a different variable, as in result = average(numbers). Calls with missing or extra arguments, such as len() and len([10], [20]), also raise TypeError. Read the message to distinguish the cause.
Dictionary keys, local variables, and assertions
With scores = {'Teal': 40}, scores['Sam'] raises KeyError. But scores[Sam] first looks for a variable named Sam. If it is undefined, Python raises NameError before trying the lookup.
UnboundLocalError is a kind of NameError. Assigning to a name inside a function normally makes it local to that function. Here score += 1 tries to read that local value before it has been assigned:
score = 5
def update():
score += 1
return score
update()The call raises:
UnboundLocalError: cannot access local variable 'score' where it is not associated with a value
Pass the old score as a parameter instead: define update(score) and call it with score = update(score).
An assert checks an assumption and raises AssertionError when the condition is false. Assertions can be disabled, so keep the explicit if and raise in average to reject empty input.
Debugging
If the report still fails, choose an action that can reveal new information. The chart compares useful debugging habits with the temptation to rerun unchanged code:
Print the values used on the failing line and check the documentation for that operation. The Python exception reference explains the error types above.
A program that keeps running without a traceback may have a loop whose condition never becomes false. Check how the loop changes the values in its condition. An exception handler cannot fix an infinite loop.
Return to the complete report after each repair and try the valid, mixed, empty, and missing files again.