Site

Exceptions — Python Exception Handling

Tutorial S4  •  Python / Learn

S4.0 What This Teaches

This tutorial covers Python exception handling: Rust uses Result<T, E> and ? for recoverable errors instead of exceptions. Python's philosophy ("Easier to Ask Forgiveness than Permission", or EAFP) embraces try/except more readily than C#.

S4.1 try / except / else / finally

try:
    x = int("abc")      # raises ValueError
    y = 10 // 0         # raises ZeroDivisionError
except ValueError as e:
    print(f"Value error: {e}")
except ZeroDivisionError:
    print("Cannot divide by zero")
except Exception as e:          # catch-all for unexpected exceptions
    print(f"Unexpected: {type(e).__name__}")
else:
    print("no exception occurred")   # only runs if no exception was raised
finally:
    print("always runs")
The else clause runs when the try block completed without raising - useful to separate success logic from error handling.

S4.2 Built-in Exception Types

ExceptionCommon cause
ValueErrorArgument has right type but bad value
TypeErrorWrong type passed to an operation
KeyErrorDict key not found
IndexErrorList or string index out of range
AttributeErrorObject doesn't have the requested attribute
FileNotFoundErrorFile or directory doesn't exist
ZeroDivisionErrorDivision or modulo by zero
StopIterationIterator has no more items
OSErrorOS-level error (file, network, permissions)
RuntimeErrorGeneral-purpose error when no better type fits

S4.3 Raising Exceptions

def square_root(x: float) -> float:
    if x < 0:
        raise ValueError(f"square_root requires x >= 0, got {x}")
    return x ** 0.5

# Re-raise the current exception
try:
    do_work()
except SomeError:
    log("error occurred")
    raise   # re-raises without losing the original traceback

# Exception chaining
try:
    data = json.loads(raw)
except json.JSONDecodeError as e:
    raise ValueError("bad config file") from e

S4.4 Custom Exception Classes

class InsufficientFundsError(Exception):
    def __init__(self, amount: float, balance: float):
        self.amount = amount
        self.balance = balance
        super().__init__(
            f"Cannot withdraw {amount:.2f}; balance is {balance:.2f}"
        )

# Usage
try:
    raise InsufficientFundsError(100.0, 50.0)
except InsufficientFundsError as e:
    print(e)                          # Cannot withdraw 100.00; balance is 50.00
    print(f"Short by {e.amount - e.balance:.2f}")
Derive custom exceptions from Exception (not BaseException). Give them a descriptive name ending in Error.

S4.5 Context Managers and with

The with statement calls __exit__ on the context manager when the block ends - even if an exception is raised. This guarantees cleanup:
# File opened and closed even if an exception occurs
with open("data.txt") as f:
    for line in f:
        print(line.strip())

# Multiple context managers on one line
with open("input.txt") as src, open("output.txt", "w") as dst:
    dst.write(src.read().upper())

# contextlib.contextmanager for custom context managers
from contextlib import contextmanager

@contextmanager
def timer():
    import time
    start = time.monotonic()
    yield
    print(f"elapsed: {time.monotonic() - start:.3f}s")

with timer():
    sum(range(10_000_000))

S4.6 Example - All Together

# Exceptions - safe integer parsing and custom validation error.

class ValidationError(ValueError):
    def __init__(self, field: str, message: str):
        self.field = field
        super().__init__(f"{field}: {message}")

def parse_age(text: str) -> int:
    try:
        age = int(text)
    except ValueError:
        raise ValidationError("age", f"'{text}' is not an integer") from None
    if not 0 <= age <= 150:
        raise ValidationError("age", f"{age} is outside valid range 0-150")
    return age

for value in ["25", "abc", "200"]:
    try:
        print(f"age: {parse_age(value)}")
    except ValidationError as e:
        print(f"error [{e.field}]: {e}")
Expected output:
age: 25
error [age]: age: 'abc' is not an integer
error [age]: age: 200 is outside valid range 0-150

S4.7 Exercise

Exercise
  • Write a function safe_divide(a, b) that returns None instead of raising ZeroDivisionError.
  • Create a custom ConfigError exception. Write a function that reads a config dict and raises ConfigError when a required key is missing.
  • Use try/except/else/finally to open a file: print its line count in else, handle FileNotFoundError in except, and print "done" in finally.

S4.8 Common Mistakes

Bare except: catches everything including KeyboardInterrupt

try:
    do_work()
except:                  # catches SystemExit, KeyboardInterrupt, etc!
    pass

except Exception:        # correct - catches application-level exceptions only
    pass

Silencing exceptions with pass

try:
    result = risky()
except Exception:
    pass   # error swallowed - caller never knows it failed
At minimum log the exception. Only silence it if you have a concrete reason the error is expected and harmless.

Raising a new exception instead of re-raising

except ValueError as e:
    raise ValueError(str(e))   # creates new exception, loses original traceback
    raise                       # correct: re-raises with original context

S4.9 Key Terms

TermMeaning
try / exceptBlock pair catching exceptions of a specified type
elseRuns after try only if no exception was raised
finallyRuns always; used for cleanup independent of exception status
raiseThrows an exception; bare raise re-raises the current one
raise ... fromChains exceptions: new exception records the cause
ExceptionBase class for application-level exceptions; all built-ins derive from it
context managerObject implementing __enter__/__exit__; used with with statement
withEnsures __exit__ (cleanup) is called even if an exception occurs