Patterns
A pattern is a few lines of code that solve a problem that comes up often.
Look Before You Leap
Look Before You Leap means checking for problems
with an if before running a line of code that can raise an
exception. The Python
glossary calls it LBYL. Each section below names an exception and
the checks that prevent it.
IndexError
Check an index against len before using it, and check
that a list has items before pop.
scores = [90, 85, 77]
i = 5
if i >= 0 and i < len(scores):
print(scores[i])
else:
print(f"No score at index {i}.")
names = []
if len(names) > 0:
print(names.pop())
else:
print("No names left.")No score at index 5.
No names left.
Without the checks, each one crashes on its own:
IndexError: list index out of range
IndexError: pop from empty list
Strings raise the same error, as in Last Character: Empty Input.
ValueError
int needs digits, max and min
need at least one item, and remove needs the item to be in
the list.
text = "12a"
if text.isdecimal():
print(int(text) * 2)
else:
print(f"{text} is not a whole number.")
scores = []
if len(scores) > 0:
print(max(scores))
else:
print("No scores yet.")
names = ["Ada", "Grace"]
if "Alan" in names:
names.remove("Alan")
else:
print("Alan is not on the list.")12a is not a whole number.
No scores yet.
Alan is not on the list.
Without the checks, each one crashes on its own:
ValueError: invalid literal for int() with base 10: '12a'
ValueError: max() iterable argument is empty
ValueError: list.remove(x): x not in list
isdecimal is the check that matches int.
"²".isdigit() is True, but
int("²") crashes (A Bug We Didn't
Handle). isdecimal also rejects "-5",
which int accepts. Enter a Valid
Integer slices off the minus sign before the check.
ZeroDivisionError
Check the number you divide by. An average divides by
len(scores), so it needs the list to have items.
total = 60
people = 0
if people > 0:
print(f"Each pays ${total / people:.2f}")
else:
print("Nobody to split the bill.")
scores = []
if len(scores) > 0:
print(f"Average: {sum(scores) / len(scores):.2f}")
else:
print("No scores yet.")Nobody to split the bill.
No scores yet.
Without the checks, both divisions crash:
ZeroDivisionError: division by zero
sum([]) is 0 and safe on its own. The
average crashes because it divides by len(scores), which is
0.
TypeError
A function that returns None to mean "no answer" needs a
check before the caller uses the answer.
def average(scores):
if len(scores) == 0:
return None
return sum(scores) / len(scores)
result = average([])
if result == None:
print("No scores.")
else:
print(f"Average: {result:.2f}")No scores.
Without the check, formatting None crashes:
TypeError: unsupported format string passed to NoneType.__format__
The function checks too: its len check prevents the
ZeroDivisionError above. Solution: Average of Scores uses
both checks.
FileNotFoundError
os.path.isfile checks that a file exists before
open reads it. Appendix K of the textbook covers the
os module.
import os
filename = "scores.csv"
if os.path.isfile(filename):
with open(filename, "r") as f:
print(f.readline().strip())
else:
print(f"Could not find {filename}")When scores.csv is not next to the program:
Could not find scores.csv
Without the check, open crashes:
FileNotFoundError: [Errno 2] No such file or directory: 'scores.csv'
For files, Easier to Ask for
Forgiveness than Permission is the safer choice. Another program can
delete the file between the check and the open.
Easier to Ask for Forgiveness than Permission
Easier to Ask for Forgiveness than Permission means
running a line of code that can raise an exception inside try and
handling the exception in except. The Python
glossary calls it EAFP. Each section below names an exception and
the except that catches it.
IndexError
An index past the end of a list and a pop from an empty
list both raise an IndexError.
scores = [90, 85, 77]
i = 5
try:
print(scores[i])
except IndexError:
print(f"No score at index {i}.")
names = []
try:
print(names.pop())
except IndexError:
print("No names left.")No score at index 5.
No names left.
A negative index counts from the end of the list, so
i = -1 prints 77 with no exception.
ValueError
int raises a ValueError for text that is
not a whole number, max and min for an empty
list, and remove for an item not in the list.
text = "12a"
try:
print(int(text) * 2)
except ValueError:
print(f"{text} is not a whole number.")
scores = []
try:
print(max(scores))
except ValueError:
print("No scores yet.")
names = ["Ada", "Grace"]
try:
names.remove("Alan")
except ValueError:
print("Alan is not on the list.")12a is not a whole number.
No scores yet.
Alan is not on the list.
int does the checking itself, so negative numbers like
"-5" work.
ZeroDivisionError
Dividing by 0 raises a ZeroDivisionError.
An average divides by len(scores), which is 0
for an empty list.
total = 60
people = 0
try:
print(f"Each pays ${total / people:.2f}")
except ZeroDivisionError:
print("Nobody to split the bill.")
scores = []
try:
print(f"Average: {sum(scores) / len(scores):.2f}")
except ZeroDivisionError:
print("No scores yet.")Nobody to split the bill.
No scores yet.
A function does not have to catch the exception itself. Python passes
it to the line that called the function, and the try around
that line catches it:
def average(scores):
return sum(scores) / len(scores)
try:
print(f"Average: {average([]):.2f}")
except ZeroDivisionError:
print("No scores.")No scores.
FileNotFoundError
open raises a FileNotFoundError when no
file has that name.
filename = "scores.csv"
try:
with open(filename, "r") as f:
print(f.readline().strip())
except FileNotFoundError:
print(f"Could not find {filename}")When scores.csv is not next to the program:
Could not find scores.csv
FileNotFoundError
in the exceptions lecture reads every line of the file the same way.