Python Arena
Step 4 of 5

How errors move from function to function

When an exception happens inside a function and the function doesn't handle it, the function stops right there, and the exception moves on to the code that called the function. If that code doesn't handle it either, it moves on to that code's caller, and so on. If nothing on the way handles it, the program stops, and the traceback lists every call on the way:

def average(numbers):
    return sum(numbers) / len(numbers)


def report(name, numbers):
    print(name, "average:", average(numbers))


report("Week 1", [3, 5, 7])
report("Week 2", [])
Output
Week 1 average: 5.0
Python stops with
Traceback (most recent call last):
  File "main.py", line 10, in <module>
    report("Week 2", [])
    ~~~~~~^^^^^^^^^^^^^^
  File "main.py", line 6, in report
    print(name, "average:", average(numbers))
                            ~~~~~~~^^^^^^^^^
  File "main.py", line 2, in average
    return sum(numbers) / len(numbers)
           ~~~~~~~~~~~~~^~~~~~~~~~~~~~
ZeroDivisionError: division by zero

Read it from the bottom up: the error happened in average on line 2, which was called by report on line 6, which was called by the main program on line 10. The line that's really wrong is often one of the callers: here it's the main program, which asked for the average of an empty list.

Handling it where you know what to do

So an exception can be handled in a different place from where it happens, and often the right place is the caller. A function that reads a file can't know whether a missing file is a disaster or just means there's no data yet, but the code that calls it does:

def read_numbers(filename):
    numbers = []
    with open(filename) as file:
        for line in file:
            numbers.append(int(line))
    return numbers


try:
    savings = read_numbers("savings.txt")
except FileNotFoundError:
    savings = []
print("Savings:", savings)
Output
Savings: []

There's no savings.txt, so open raises a FileNotFoundError inside read_numbers. The function stops at once (it never gets to its return), and the exception arrives at the try around the call, whose except decides what to do.

What doesn't run

When an exception leaves a try block, the rest of the block is skipped, including any later calls:

def check(value):
    print("Checking", value)
    number = int(value)
    print("Valid:", number)
    return number


try:
    check("12")
    check("twelve")
    check("7")
except ValueError:
    print("Stopped at an invalid value")
print("Done")
Output
Checking 12
Valid: 12
Checking twelve
Stopped at an invalid value
Done

check("twelve") stops at int, so it never prints Valid:, and check("7") is never called at all. To check every value whatever happens, the try would go inside a loop, around each call.

Your task

Challenge 1 of 3

The function load_scores(filename) reads whole numbers from a file. Add error handling to the main program, not to the function: a missing file prints Could not find and the file name, and a line that isn't a whole number prints Bad data in and the file name. A run with a good file looks like this:

Scores file: scores.txt
Best score: 87

Challenge 2

What does this program print? Type each line it prints.

Challenge 3

The starter code has read_numbers(filename), which returns the numbers of a file as a list and raises a FileNotFoundError if the file doesn't exist. Write total_of_files(filenames), which gets a list of file names and returns the sum of all the numbers in all of them. A file that doesn't exist counts as 0. Handle the error in total_of_files, and don't change read_numbers.

With a.txt holding 3 and 4 on two lines and b.txt holding 10, total_of_files(["a.txt", "b.txt", "c.txt"]) returns 17.