Skip to content

Catching errors with try and except

Goal: catch an exception with try and except, so that bad input or a missing file gets a friendly message instead of a crash.

Why this matters

In the last lesson an exception always meant the end of the program. That is fine while you are the only user, because you can read the traceback and fix the code. But some problems are not mistakes in your code at all. A user types abc where you asked for a number. A file has been moved. You cannot prevent these, and the person using your program should never have to see a traceback because of them. This lesson shows you how to expect trouble and deal with it.

The problem

Here is the smallest program that a user can crash:

crash.py
age = int(input("How old are you? "))
print(f"Next year you will be {age + 1}.")
How old are you? abc
Traceback (most recent call last):
  File "crash.py", line 1, in <module>
    age = int(input("How old are you? "))
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: invalid literal for int() with base 10: 'abc'

You can read that now: a ValueError, on line 1, because int() cannot turn 'abc' into a number. There is no bug to fix here. The code is correct and the input is wrong, so the program needs a plan for bad input.

try and except

The plan is written with two new keywords:

try_basic.py
text = input("How old are you? ")

try:
    age = int(text)
    print(f"Next year you will be {age + 1}.")
except ValueError:
    print("That is not a whole number.")

print("Thanks for visiting.")

Read it as: try to do this, and if a ValueError happens, do that instead. A run with good input:

How old are you? 36
Next year you will be 37.
Thanks for visiting.

And with bad input:

How old are you? abc
That is not a whole number.
Thanks for visiting.

No traceback. Here is exactly what Python does:

  • It runs the indented lines under try: one at a time, as normal.
  • If none of them causes an error, the except block is skipped, and the program carries on below it.
  • If one of them causes a ValueError, Python stops the try block right there, jumps to the except ValueError: block, and runs that instead. The rest of the try block never runs. That is why Next year you will be did not appear in the second run: int(text) failed, so Python never reached the print below it.
  • Either way, the program then continues after the whole try/except. Thanks for visiting. appears in both runs.

When an except block deals with an exception like this, programmers say the exception was caught, or handled. An exception that nobody catches crashes the program, as before.

Asking again

A message is better than a crash, but better still is a second chance. Put the try inside a while True loop and the program keeps asking until it gets something it can use:

ask_again.py
def ask_for_number(prompt):
    """Keep asking until the user types a whole number, then return it."""
    while True:
        text = input(prompt)
        try:
            return int(text)
        except ValueError:
            print("Please type a whole number.")

temperature = ask_for_number("Temperature outside: ")
print(f"Tomorrow will be {temperature + 2} degrees.")
Temperature outside: warm
Please type a whole number.
Temperature outside: 12.5
Please type a whole number.
Temperature outside: -3
Tomorrow will be -1 degrees.

If int(text) works, return hands the number back, which ends the function and the loop with it. If it fails, Python jumps to the except block, prints the message, and the loop goes round again.

You wrote ask_for_number() once before, in Module 5, using .isdigit() to check the text first. That version had a flaw you may have noticed: it refused -3, because a minus sign is not a digit. This version has no such problem. It does not try to guess in advance what int() will accept. It simply lets int() try, and deals with a refusal. Change int to float and you have a function that accepts 12.5 as well.

This is the version of ask_for_number() to keep. Use it from now on.

Always name the error

You can leave the error type out and write a plain except:, which catches everything. It looks convenient. It is a trap. This program contains a bug:

bare_except.py
try:
    number = int(input("Number: "))
    print(f"Double is {numbr * 2}")
except:
    print("That is not a number.")
Number: 5
That is not a number.

The user typed a perfectly good number and was told it is not one. What happened? Look closely at line 3: numbr is misspelled. That causes a NameError, the plain except: caught it along with everything else, and the program printed a message that has nothing to do with the real problem. You could lose an hour hunting for this.

Now the same program with the error named:

named_except.py
try:
    number = int(input("Number: "))
    print(f"Double is {numbr * 2}")
except ValueError:
    print("That is not a number.")
Number: 5
Traceback (most recent call last):
  File "named_except.py", line 3, in <module>
    print(f"Double is {numbr * 2}")
                       ^^^^^
NameError: name 'numbr' is not defined. Did you mean: 'number'?

This is better, even though it crashes. except ValueError: catches the one problem you planned for, and lets everything else through. The traceback you learned to read last lesson takes you straight to the bug.

So there are two rules:

  • Always name the error type you expect. Catch the problems you planned for, and let the surprises crash, because a crash with a traceback is far easier to fix than a program that quietly does the wrong thing.
  • Keep the try block small. Put in it only the lines that can fail in the way you expect. The misspelled print had no business being inside the try at all.

How do you know which error type to name? Make it happen once, on purpose, and read the last line of the traceback.

When a file is missing

In Module 6 a misspelled filename ended the program with FileNotFoundError. The same loop gives the user another go. This needs shopping.txt from Module 6 in the same folder:

open_file.py
def read_lines(filename):
    """Return the lines of a file as a list, without the newline characters."""
    lines = []
    with open(filename) as file:
        for line in file:
            lines.append(line.strip())
    return lines

def ask_for_file():
    """Keep asking for a filename until one can be read, then return its lines."""
    while True:
        filename = input("File to read: ")
        try:
            return read_lines(filename)
        except FileNotFoundError:
            print(f"There is no file called {filename}. Try again.")

lines = ask_for_file()
print(f"Read {len(lines)} lines. The first is: {lines[0]}")
File to read: shoping.txt
There is no file called shoping.txt. Try again.
File to read: shopping
There is no file called shopping. Try again.
File to read: shopping.txt
Read 5 lines. The first is: eggs

Notice where the error starts and where it is caught. open() fails inside read_lines(), but the try is in ask_for_file(). That works because an exception travels back along the trail of calls you saw in the traceback, from the function where it happened to the function that called it, and so on, until it finds a try with a matching except. Only if it gets all the way out without finding one does the program crash.

You already know another way to handle a missing file: check with os.path.exists() before opening. Both are fine. Checking first works when there is a simple question to ask. try/except works for anything that can go wrong, including the many cases where there is nothing to check in advance.

More than one kind of error

A try can have several except blocks, one for each kind of trouble. Python runs the first one that matches:

two_errors.py
try:
    sweets = int(input("How many sweets? "))
    children = int(input("How many children? "))
    print(f"Each child gets {sweets / children} sweets.")
except ValueError:
    print("Please use whole numbers.")
except ZeroDivisionError:
    print("There is nobody to share with.")
How many sweets? 12
How many children? four
Please use whole numbers.
How many sweets? 12
How many children? 0
There is nobody to share with.

Different problems get different messages, which is the point of naming them. If two kinds of error deserve the same response, list them together in parentheses: except (ValueError, ZeroDivisionError):.

Using the error message

Every exception carries the message you saw on the last line of the traceback. Add as and a variable name to get hold of it. Here is a scores file in which two lines have gone wrong:

scores_messy.txt
Ada,82
Grace,ninety-five
Linus,77
Guido
Margaret,91

In Module 6, a single bad line like these would have crashed the whole program. Now it can skip the bad lines, say why, and carry on:

skip_bad_lines.py
total = 0
count = 0
line_number = 0

with open("scores_messy.txt") as file:
    for line in file:
        line_number += 1
        try:
            name, score = line.strip().split(",")
            score = int(score)
        except ValueError as error:
            print(f"Skipping line {line_number}: {error}")
            continue
        print(f"{name} scored {score}")
        total += score
        count += 1

print(f"Average of {count} good lines: {total / count}")
Ada scored 82
Skipping line 2: invalid literal for int() with base 10: 'ninety-five'
Linus scored 77
Skipping line 4: not enough values to unpack (expected 2, got 1)
Margaret scored 91
Average of 3 good lines: 83.33333333333333

except ValueError as error puts the exception into the variable error, and printing it shows its message. The two bad lines failed in different ways, one in int() and one because there was no comma to split on, but both are a ValueError, so one except handles both. continue, from Module 3, jumps to the next line of the file.

Python's messages are written for programmers, so show them to yourself in reports like this one. For the person using your program, write a message in plain words, as the earlier examples do.

else and finally

Two optional extras complete the picture. You will need them less often, but you should recognise them:

else_finally.py
text = input("Price: ")

try:
    price = float(text)
except ValueError:
    print("That is not a price.")
else:
    print(f"With tax that is {round(price * 1.2, 2)}.")
finally:
    print("Thank you, goodbye.")
Price: 10
With tax that is 12.0.
Thank you, goodbye.
Price: ten
That is not a price.
Thank you, goodbye.
  • The else block runs only if the try block finished with no error. It is the natural home for the code that depends on the try having worked, and it keeps the try block small, as the rule says.
  • The finally block runs every time, error or not. It is for tidying up that must happen whatever else does.

Project: a to-do app that does not crash

At the end of Module 6 you were asked to notice what happens when the user types abc at Which number is done? The answer was a traceback, and the loss of every task added since the program started. Here is the repaired app:

todo_safe.py
FILENAME = "todo.txt"

def load_tasks():
    """Return the saved tasks as a list, or an empty list if there is no file yet."""
    tasks = []
    try:
        with open(FILENAME) as file:
            for line in file:
                tasks.append(line.strip())
    except FileNotFoundError:
        print("No saved tasks yet. Starting a new list.")
    return tasks

def save_tasks(tasks):
    """Write the tasks to the file, one per line."""
    with open(FILENAME, "w") as file:
        for task in tasks:
            file.write(task + "\n")

def ask_for_number(prompt):
    """Keep asking until the user types a whole number, then return it."""
    while True:
        text = input(prompt)
        try:
            return int(text)
        except ValueError:
            print("Please type a whole number.")

def show_tasks(tasks):
    if len(tasks) == 0:
        print("Nothing to do!")
    number = 1
    for task in tasks:
        print(f"{number}. {task}")
        number += 1

def finish_task(tasks):
    if len(tasks) == 0:
        print("Nothing to finish!")
        return
    show_tasks(tasks)
    number = ask_for_number("Which number is done? ")
    if number >= 1 and number <= len(tasks):
        finished = tasks.pop(number - 1)
        print(f"Finished: {finished}")
    else:
        print(f"There is no task {number}.")

def show_menu():
    print()
    print("1. Show tasks")
    print("2. Add a task")
    print("3. Finish a task")
    print("4. Quit")
    return input("Choose: ")

tasks = load_tasks()
print(f"Loaded {len(tasks)} tasks.")

while True:
    choice = show_menu()
    if choice == "1":
        show_tasks(tasks)
    elif choice == "2":
        tasks.append(input("New task: "))
    elif choice == "3":
        finish_task(tasks)
    elif choice == "4":
        save_tasks(tasks)
        print(f"Saved {len(tasks)} tasks. Goodbye.")
        break
    else:
        print("Please choose 1, 2, 3, or 4.")
No saved tasks yet. Starting a new list.
Loaded 0 tasks.

1. Show tasks
2. Add a task
3. Finish a task
4. Quit
Choose: 2
New task: buy milk

1. Show tasks
2. Add a task
3. Finish a task
4. Quit
Choose: 2
New task: call mum

1. Show tasks
2. Add a task
3. Finish a task
4. Quit
Choose: 3
1. buy milk
2. call mum
Which number is done? abc
Please type a whole number.
Which number is done? 7
There is no task 7.

1. Show tasks
2. Add a task
3. Finish a task
4. Quit
Choose: 4
Saved 2 tasks. Goodbye.

Three things changed:

  • load_tasks() tries to open the file and catches FileNotFoundError, instead of checking with os.path.exists(). The import os line is gone.
  • ask_for_number() from this lesson replaces the bare int(input(...)), so abc gets a second chance.
  • Finishing a task moved into its own function, finish_task(), which checks that the number is in range.

That last check deserves a closer look, because it uses an if and not a try. Task 7 of 2 would cause an IndexError, and you could catch that. But what about 0? The program would run tasks.pop(-1). A negative index counts from the end of a list, so that is valid Python. It causes no error, and it quietly deletes the last task. That is a logic error, the sneaky kind from the last lesson, and try/except cannot help with an error that never happens.

So the two tools divide the work. Use try/except for problems that Python reports as exceptions. Use if for values that Python accepts and you do not.

Try it

Run try_basic.py and type your age, then a word, then nothing at all: just press Enter. Then change except ValueError: to except IndexError:, type a word again, and see what happens when the except does not match the error. Finally, add the improved ask_for_number() to one of your own earlier programs that uses int(input(...)).

Common mistakes

My except block never runs and the program still crashes

The except names a different error from the one that happened. except ValueError: does nothing for a FileNotFoundError. Read the last line of the traceback and name that type.

The program says the input is wrong when it is not

A plain except:, or a try block with too much in it, is catching an error you did not plan for, probably a bug of your own. Name the error type and move everything you can out of the try block.

NameError: name 'age' is not defined, after the try

If age = int(text) fails, age is never created, and then a line after the try/except uses it. Put the lines that need age in an else block, or loop until the input is good, as ask_for_number() does.

SyntaxError: multiple exception types must be parenthesized

You wrote except ValueError, IndexError:. Several types need parentheses: except (ValueError, IndexError):.

SyntaxError: expected 'except' or 'finally' block

A try: cannot stand alone. It must be followed by an except or a finally, at the same indentation as the try.

Wrapping the whole program in one big try

It stops the crashes and hides every bug you ever write. Catch specific errors, in specific places, where you have a sensible response. If you do not know what to do about an error, let it crash.

Exercises

  1. Birth year. Ask what year the user was born and print the age they will turn in 2050. If they do not type a whole number, print a helpful message that includes what they typed. No loop needed.
  2. Tip calculator. Write ask_for_float(prompt), which keeps asking until it gets a number and accepts decimals. Use it to ask for a bill and a tip percent, then print the tip and the total, rounded to 2 decimal places.
  3. Safe total. Save the file below as prices_messy.txt. Ask for a filename until the user gives one that exists. Add up every line that is a valid price, and report the total and how many lines were skipped.

    prices_messy.txt
    4.50
    3.20
    free
    2.00
    
    12,99
    1.30
    
  4. Unbreakable high score. The high score loader from Module 6 crashes if high_score.txt contains something that is not a number. Rewrite load_high_score() without os.path.exists(). It should return 0 if the file is missing, and also return 0, with a warning, if the contents are not a number. Test it by typing a word into the file.

Solution 1
text = input("What year were you born? ")

try:
    year = int(text)
    print(f"In 2050 you will turn {2050 - year}.")
except ValueError:
    print(f"'{text}' is not a year. Use digits, like 1990.")

The input is kept in text before converting, so the except block can show the user what they typed.

Solution 2
def ask_for_float(prompt):
    """Keep asking until the user types a number, then return it."""
    while True:
        text = input(prompt)
        try:
            return float(text)
        except ValueError:
            print("Please type a number, such as 12.50")

bill = ask_for_float("Bill: ")
percent = ask_for_float("Tip percent: ")
tip = bill * percent / 100
print(f"Tip: {round(tip, 2)}")
print(f"Total: {round(bill + tip, 2)}")

It is ask_for_number() with float in place of int. Typing 15% is refused, because float() does not understand the percent sign, and the user is simply asked again.

Solution 3
def read_lines(filename):
    """Return the lines of a file as a list, without the newline characters."""
    lines = []
    with open(filename) as file:
        for line in file:
            lines.append(line.strip())
    return lines

def ask_for_file():
    """Keep asking for a filename until one can be read, then return its lines."""
    while True:
        filename = input("Price file: ")
        try:
            return read_lines(filename)
        except FileNotFoundError:
            print(f"There is no file called {filename}. Try again.")

total = 0
skipped = 0
for line in ask_for_file():
    try:
        total += float(line)
    except ValueError:
        skipped += 1

print(f"Total: {round(total, 2)}")
print(f"Skipped {skipped} bad lines.")

The total is 11.0 with 3 lines skipped: the word free, the blank line, and 12,99, because Python only accepts a point in decimal numbers. There are two separate try blocks, each small, each with one job: one for the missing file and one for bad lines.

Solution 4
FILENAME = "high_score.txt"

def load_high_score():
    """Return the saved best score, or 0 if there is no usable record."""
    try:
        with open(FILENAME) as file:
            return int(file.read().strip())
    except FileNotFoundError:
        return 0
    except ValueError:
        print("The high score file is damaged. Starting again from no record.")
        return 0

record = load_high_score()
if record == 0:
    print("No record yet.")
else:
    print(f"The record is {record} guesses.")

A missing file is normal on the first run, so it gets no message. A damaged file is worth a warning. They get separate except blocks because they deserve different responses.

Summary

  • try: runs code that might fail. except SomeError: says what to do if it does. The program then carries on.
  • When an error happens, the rest of the try block is skipped.
  • Always name the error type, and keep the try block small. A plain except: hides your own bugs.
  • A try inside while True gives the user another chance. Keep ask_for_number() for every program that needs a number.
  • Several except blocks handle different errors differently. as error gives you the message.
  • else runs when nothing went wrong, and finally runs every time.
  • try/except is for problems Python reports. Use if for values that Python accepts and you do not.

Module 7 complete

Your programs now cope with a world that does not behave: users who type nonsense, files that go missing, data with mistakes in it. Along the way you have used a few tools that you did not write yourself, random and os. Module 8 opens that toolbox properly and shows you what else comes free with Python.

Next: Borrowing code with import