Exception Handling

Python Intermediate
Course 2 ยท Chapter 3 ยท Exception Handling

๐Ÿšจ Exception Handling

You've already seen a few exceptions crash a program by accident โ€” a KeyError from a missing dict key (Course 1, Chapter 7), a TypeError from mutating a tuple. This chapter covers handling them deliberately: try/except/else/finally, raising your own exceptions, and defining custom exception types.

๐Ÿ›ก๏ธ try/except Basics

try:
    result = 10 / 0
except ZeroDivisionError:
    print("Can't divide by zero!")

# catching multiple, specific exception types:
try:
    value = int(input("Enter a number: "))
except ValueError:
    print("That wasn't a valid number")
except KeyboardInterrupt:
    print("Input cancelled")

Code inside try runs normally until an exception occurs; the matching except block then runs instead of letting the program crash. Multiple except blocks let you handle different failure types differently.

โš  A Bare except: Catches Everything โ€” Including Things You Don't Want
try:
    risky_operation()
except:   # catches ALL exceptions, even ones you didn't anticipate
    print("something went wrong")

A bare except: with no exception type swallows everything โ€” including typos that raise NameError, programming bugs that raise TypeError, and even KeyboardInterrupt (Ctrl+C) and SystemExit, making the program impossible to stop cleanly. Always catch specific exception types (except ValueError:), or at minimum except Exception:, which excludes those system-level signals.

๐Ÿ”€ else and finally

try:
    value = int("42")
except ValueError:
    print("conversion failed")
else:
    print(f"conversion succeeded: {value}")   # runs only if NO exception occurred
finally:
    print("this always runs")                 # runs no matter what

This is the same else-means-"no exception fired" idea as the loop else clause from Course 1, Chapter 4 โ€” else here runs only when the try block completed with no exception. finally always runs, whether an exception occurred or not โ€” the standard place for cleanup code (closing a file or network connection) that must happen either way.

๐Ÿ“ข Raising Exceptions

def withdraw(balance, amount):
    if amount > balance:
        raise ValueError("Insufficient funds")
    return balance - amount

try:
    withdraw(100, 150)
except ValueError as e:
    print(f"Error: {e}")   # Error: Insufficient funds

raise triggers an exception deliberately โ€” as e captures the exception object so you can inspect its message with str(e) or, as shown, directly in an f-string.

๐Ÿท๏ธ Custom Exceptions

class InsufficientFundsError(Exception):
    pass

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(f"Cannot withdraw {amount}, balance is only {balance}")
    return balance - amount

try:
    withdraw(100, 150)
except InsufficientFundsError as e:
    print(e)   # Cannot withdraw 150, balance is only 100

A custom exception is just a class inheriting from Exception (usually with nothing else needed โ€” pass is enough) โ€” reusing the exact class/inheritance mechanics from Chapter 2. Naming it specifically (InsufficientFundsError rather than a generic ValueError) makes both the raising code and the catching code more self-documenting.

Error Handling: Go vs Kotlin vs Python

LanguageApproach
GoNo exceptions at all โ€” functions return an explicit (value, error) pair, and the caller checks if err != nil after every call. Failure is just a normal return value.
KotlinExceptions, like Java โ€” but all unchecked (no throws declarations the compiler enforces), so a function's signature alone never tells you what it might throw.
PythonExceptions, always unchecked, propagating up the call stack until caught by a try/except or crashing the program.

Go's explicit-error-return style is a genuinely different philosophy from Python's โ€” every Go function that can fail says so directly in its return type, where Python (and Kotlin) require reading the implementation or documentation to know what might be raised.

try / except

Catches specific exception types; avoid a bare except:.

else / finally

else runs only on success; finally always runs, for cleanup.

raise

Triggers an exception deliberately, optionally with a message.

Custom exceptions

A class inheriting from Exception โ€” self-documenting failure types.

๐Ÿ’ป Coding Challenges

Challenge 1: Safe Division Function

Write a function safe_divide(a, b) that returns a / b, but catches ZeroDivisionError and returns None instead of crashing when b is 0. Call it with (10, 2) and (10, 0), printing both results.

Goal: Practice basic try/except around a real failure case.

โ†’ Solution

Challenge 2: Validated Age Input

Write a function set_age(age) that raises a ValueError with the message "Age cannot be negative" if age < 0, and otherwise returns the age unchanged. Call it with a valid age inside a try/except, then with -5, printing the caught error message.

Goal: Practice raise-ing a built-in exception type with a custom message, and catching it with as e.

โ†’ Solution

Challenge 3: Custom Exception for Stock Levels

Define a custom exception OutOfStockError(Exception). Write a function purchase(stock, quantity) that raises it (with a descriptive message) if quantity > stock, and otherwise returns stock - quantity. Demonstrate it raising and being caught.

Goal: Practice defining and using a custom exception end to end.

โ†’ Solution

๐ŸŽฏ What's Next

Next chapter: File I/O โ€” reading/writing files, context managers (the with statement), and pathlib.