Errors and exceptions
Users type the wrong thing, files go missing and networks drop. Good programs expect this and respond sensibly instead of crashing.
Reading a traceback
When an error is not handled, Python prints a traceback:
Traceback (most recent call last):
File "grades.py", line 7, in <module>
print(average([]))
File "grades.py", line 2, in average
return sum(scores) / len(scores)
ZeroDivisionError: division by zero
Read it from the bottom up. The last line names the exception and what went wrong. The lines above show the chain of calls that led there, with the most recent call last. Here, average was called with an empty list on line 7.
Common exceptions you will meet: ValueError (right type, bad value, like int("abc")), TypeError (wrong type, like "a" + 1), KeyError, IndexError, FileNotFoundError and ZeroDivisionError.
try and except
while True:
text = input("Enter your age: ")
try:
age = int(text)
break
except ValueError:
print(f"{text!r} is not a whole number, try again.")
Python runs the try block. If a ValueError happens inside it, Python jumps to the matching except block instead of crashing.
Always name the exception you expect. A bare except: catches everything, including typos in your own code and Ctrl + C, which hides bugs.
else and finally
try:
f = open("data.txt", encoding="utf-8")
except FileNotFoundError:
print("No data file yet.")
else:
print(f.read()) # runs only if no exception happened
f.close()
finally:
print("Done.") # always runs
Keep the try block small, so you only catch errors from the line you expected to fail. finally is for clean-up that must happen either way. (For files, a with block already does that clean-up.)
Raising exceptions
Your own functions should raise an exception when they are given something they cannot handle, rather than returning a wrong answer quietly:
def average(scores):
if not scores:
raise ValueError("cannot average an empty list")
return sum(scores) / len(scores)
The caller can then decide what to do: catch it, show a message or let the program stop.
Custom exceptions
For your own projects, you can define exception types by inheriting from Exception. Callers can then catch exactly your error:
class InsufficientFunds(Exception):
pass
def withdraw(balance, amount):
if amount > balance:
raise InsufficientFunds(f"balance {balance}, tried {amount}")
return balance - amount
try:
withdraw(30, 50)
except InsufficientFunds as e:
print("Declined:", e) # Declined: balance 30, tried 50
Ask forgiveness, not permission
Python style often tries the operation and handles failure, rather than checking everything first:
# Checking first
if "email" in user:
send(user["email"])
# Trying, then handling failure
try:
send(user["email"])
except KeyError:
print("No email on file.")
Both are fine for dictionaries. For files and networks, trying is safer: a file can be deleted between your check and your open.
Exercises
1. Safe division
Write safe_divide(a, b) that returns a / b, or None if b is zero, using try and except.
Show answer
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
safe_divide(10, 4) # 2.5
safe_divide(1, 0) # None
2. Read a number from a file
Write read_number(path) that returns the integer stored in a text file. It should return 0 if the file does not exist, and raise a ValueError with a clear message if the file contains something that is not a number.
Show answer
def read_number(path):
try:
with open(path, encoding="utf-8") as f:
text = f.read().strip()
except FileNotFoundError:
return 0
try:
return int(text)
except ValueError:
raise ValueError(f"{path} does not contain a number: {text!r}")
Two small try blocks keep each one about a single kind of failure.
3. Find the bug
This code is meant to skip bad lines, but it hides a real bug. What is it?
total = 0
for line in lines:
try:
total += int(lnie)
except:
pass
Show answer
lnie is a typo, which raises a NameError on every line. The bare except swallows it, so total silently stays 0. Write except ValueError: and the typo shows up immediately.