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.
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.
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, /.
# Your code hereShow 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.