Part 3: Python Programming

13. Exceptions & Error Handling

Exceptions & Error Handling

Things go wrong: a file is missing, a user types "cat" where a number was expected, a calculation divides by zero. When Python hits such a problem it raises an exception, and unless you handle it, your program stops with an error message. This chapter shows how to catch exceptions and respond gracefully.

Why handle errors?

An unhandled exception crashes your program and shows a scary traceback to the user. Handling errors lets your program recover, ask the user to try again, or save their work. Consider this crash:

PYTHON
number = int("hello")
▸ OUTPUT
ValueError: invalid literal for int() with base 10: 'hello'

try and except

Wrap risky code in a try block. If an exception occurs, Python jumps to the matching except block instead of crashing:

PYTHON
text = "hello"
try:
    number = int(text)
    print(number)
except ValueError:
    print("That was not a valid number.")
▸ OUTPUT
That was not a valid number.

It is best to catch the specific exception you expect, like ValueError, rather than catching everything. That way genuine, unexpected bugs still surface instead of being silently swallowed.

Common exception types

ExceptionHappens when...Example
ValueErrorRight type, wrong valueint("abc")
TypeErrorWrong type used"a" + 1
ZeroDivisionErrorDividing by zero5 / 0
IndexErrorList index out of range[1, 2][5]
KeyErrorMissing dictionary key{}["x"]
FileNotFoundErrorOpening a missing fileopen("no.txt")

else and finally

A full error-handling statement can have four parts. The else block runs only if no exception occurred. The finally block always runs, whether or not there was an error, which is ideal for cleanup:

PYTHON
try:
    value = 10 / 2
except ZeroDivisionError:
    print("Cannot divide by zero.")
else:
    print(f"Success: {value}")
finally:
    print("Done checking.")
▸ OUTPUT
Success: 5.0
Done checking.

Catching more than one exception

You can handle several exception types, each with its own response, or group them:

PYTHON
def safe_divide(a, b):
    try:
        return a / b
    except ZeroDivisionError:
        return "Cannot divide by zero"
    except TypeError:
        return "Please use numbers"

print(safe_divide(10, 2))
print(safe_divide(10, 0))
print(safe_divide(10, "x"))
▸ OUTPUT
5.0
Cannot divide by zero
Please use numbers

Raising your own exceptions

Sometimes your own code discovers a problem and should signal it. Use the raise keyword to raise an exception with a helpful message. This lets callers handle the problem the same way they handle built-in errors:

PYTHON
def set_age(age):
    if age < 0:
        raise ValueError("Age cannot be negative")
    return age

try:
    set_age(-5)
except ValueError as e:
    print("Error:", e)
▸ OUTPUT
Error: Age cannot be negative

Writing except ValueError as e captures the exception object in e, so you can print its message. A common real-world use is validating user input: ask again until the value makes sense.

Good error handling is the difference between a fragile program and a robust one. Catch the specific errors you expect, give the user a clear message, and let unexpected bugs surface so you can fix them.