Errors are not failures of your program — they are messages from it. Today you learn to read them and to decide which ones your code should handle.
Warm-up: from Days 1 to 4
From memory first. These mix days on purpose: real programs don’t use one idea at a time.
1. Predict all three lines.
def discount(price, rate=0.1):
return round(price * (1 - rate), 2)
prices = [20, 50, 5]
print([discount(p) for p in prices])
print(discount(50, rate=0.5))
print(max(prices, key=lambda p: -p))Check your prediction
[18.0, 45.0, 4.5]
25.0
5The last one is the odd one out: the key makes every price negative, and the largest negative number belongs to the smallest price.
2. Fix. Each call should start a fresh list: the second line should be
['pay rent'].
def add_task(task, tasks=[]):
tasks.append(task)
return tasks
print(add_task("wash up"))
print(add_task("pay rent"))Check your fix
def add_task(task, tasks=None):
if tasks is None:
tasks = []
tasks.append(task)
return tasks
print(add_task("wash up"))
print(add_task("pay rent"))['wash up']
['pay rent']The default list is made once, when the function is defined, and every call
shares it. None as the default, and a new list inside, gives each call its
own.
3. Write a function initials(name) that returns a person’s initials in
capitals, so that this line prints AL GBH:
# def initials(name): ...
print(initials("ada lovelace"), initials("Grace Brewster Hopper"))Check your answer
def initials(name):
return "".join(word[0].upper() for word in name.split())
print(initials("ada lovelace"), initials("Grace Brewster Hopper"))AL GBHSplit into words, take each word’s first letter, and join them with nothing in between. A loop that appends to a string works just as well.
Reading a traceback
def average(values):
return sum(values) / len(values)
def report(readings):
return f"Average: {average(readings)}"
print(report([]))Read a traceback from the bottom up. The last line names the error type and
message — ZeroDivisionError: division by zero. Above it, the call chain shows
where it happened: inside average, called from report, called from the last
line of your file. The bottom tells you what; the lines above tell you how
you got there.
Common exception types
| Exception | Typical cause |
|---|---|
SyntaxError | The code is not valid Python — nothing ran at all |
NameError | A name that does not exist (usually a typo) |
TypeError | An operation on the wrong type: "3" + 3 |
ValueError | Right type, impossible value: int("abc") |
IndexError | List index out of range |
KeyError | Dictionary key not present |
ZeroDivisionError | Division by zero |
FileNotFoundError | Opening a file that is not there |
tries = [
lambda: int("abc"),
lambda: [1, 2, 3][10],
lambda: {"a": 1}["b"],
lambda: "3" + 3,
]
for attempt in tries:
try:
attempt()
except Exception as error:
print(f"{type(error).__name__}: {error}")try / except
try runs code that might fail; except handles the failure instead of
crashing.
while True:
raw = input("Enter a number: ")
try:
number = int(raw)
break
except ValueError:
print(f" {raw!r} is not a number - try again")
print("You entered", number)Catch the specific exception you expect. A bare except: swallows everything
— including the KeyboardInterrupt a user pressed to stop your program, and
the typo you have not found yet.
except: pass is where bugs go to hide
Silencing an error you did not anticipate means the program continues in a state you never designed for. At minimum, log it. Better: catch only what you can genuinely handle, and let everything else surface.
else and finally
def divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print(" cannot divide by zero")
return None
else:
print(" division succeeded") # runs only when nothing was raised
return result
finally:
print(" finally always runs") # cleanup, success or failure
print(divide(10, 2))
print(divide(10, 0))else holds the code that should run only when the try succeeded. finally
runs either way, which is where cleanup belongs — closing files, releasing
locks, restoring state.
Catching several
records = {"ada": "1815", "alan": "not a year"}
for name in ["ada", "alan", "grace"]:
try:
year = int(records[name])
except KeyError:
print(f"{name}: no record")
except ValueError:
print(f"{name}: record is not a number")
else:
print(f"{name}: born {year}")You can also group them — except (KeyError, ValueError) as error: — when the
response is the same.
Raising your own
raise signals a problem your function cannot sensibly solve.
def set_age(age):
if not isinstance(age, int):
raise TypeError(f"age must be an int, got {type(age).__name__}")
if age < 0:
raise ValueError(f"age cannot be negative: {age}")
return age
print(set_age(36))
try:
set_age(-5)
except ValueError as error:
print("Rejected:", error)
set_age("thirty")Raising early, with a message that names the offending value, turns a mystery crash three functions later into an obvious one right here.
Ask forgiveness, not permission
Python style prefers trying the operation and catching the failure over
checking every precondition first. try: value = d[key] reads better than a
if key in d dance, and avoids a race where the answer changes in between.
Before you write your own
Predict. Four lines are printed. The order is the whole point.
def parse(text):
try:
value = int(text)
except ValueError:
return "bad"
else:
return value * 2
finally:
print("checked", text)
print(parse("21"))
print(parse("x"))Check your prediction
checked 21
42
checked x
badfinally runs on the way out of the function — after the return value is
decided but before the caller gets it and prints it. So each checked line
comes first.
Fix. This should skip the entry that isn’t a number, say so, and print
ages: [36, 41]. It crashes instead. Read the last line of the traceback.
raw = ["36", "unknown", "41"]
ages = []
for item in raw:
try:
ages.append(int(item))
except TypeError:
print(f"skipped: {item!r}")
print("ages:", ages)Check your fix
raw = ["36", "unknown", "41"]
ages = []
for item in raw:
try:
ages.append(int(item))
except ValueError:
print(f"skipped: {item!r}")
print("ages:", ages)skipped: 'unknown'
ages: [36, 41]int("unknown") raises ValueError — right type, impossible value — so an
except TypeError never matches it. The traceback names the exception that
actually happened: catch that one.
A robust calculator
Read two numbers and an operator, and print the result. Handle bad numbers, an unknown operator, and division by zero — each with a distinct message, and without crashing.
The pre-filled input is 10, 0, /, so your program should report that it
can’t divide by zero. Then try ten as the first number, and % as the
operator: each should get its own message.
# Your code hereHint 1: the idea
Three different things can go wrong, and two of them already have exceptions:
float("ten") raises ValueError, and dividing by zero raises
ZeroDivisionError. An unknown operator is only wrong because you say so, so
you raise something yourself. Then one try, with an except for each kind
of problem.
Hint 2: a first step
Get the happy path working with no error handling at all: read a, b and
op, then an if/elif chain for the four operators. Test it with 10, 4,
/. Only then wrap it in try: and add the except clauses one at a time,
testing each with input that triggers it.
Show one solution
try:
a = float(input("First number: "))
b = float(input("Second number: "))
op = input("Operator (+ - * /): ").strip()
if op == "+":
result = a + b
elif op == "-":
result = a - b
elif op == "*":
result = a * b
elif op == "/":
result = a / b
else:
raise ValueError(f"unknown operator {op!r}")
except ValueError as error:
print("Bad input:", error)
except ZeroDivisionError:
print("Cannot divide by zero")
else:
print(f"Result: {result:g}")Note that raise ValueError for the unknown operator is caught by the same
handler as a bad number — both really are “the input made no sense”, so one
message covers them.
What you learned
- Read tracebacks bottom-up: the last line is the error, above it is the path.
try/except SpecificErrorhandles failures you expect.elseruns on success;finallyruns regardless.raisereports problems your code cannot resolve, with a useful message.- Never silence an exception you did not plan for.