Errors & Exceptions

The Course
January 9, 2026
5 min read

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

An error, in full Python
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

ExceptionTypical cause
SyntaxErrorThe code is not valid Python — nothing ran at all
NameErrorA name that does not exist (usually a typo)
TypeErrorAn operation on the wrong type: "3" + 3
ValueErrorRight type, impossible value: int("abc")
IndexErrorList index out of range
KeyErrorDictionary key not present
ZeroDivisionErrorDivision by zero
FileNotFoundErrorOpening a file that is not there
Meet a few Python
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.

Recovering Python
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

The full shape Python
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

Different handling per error Python
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.

Refusing bad input Python
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.

Exercise

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 here
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 SpecificError handles failures you expect.
  • else runs on success; finally runs regardless.
  • raise reports problems your code cannot resolve, with a useful message.
  • Never silence an exception you did not plan for.
Last updated on January 9, 2026

Was this article helpful?

Your response is saved on this device.