Lesson 16 — Errors

Exception Handling

Python's try/except/else/finally handles errors elegantly. The else clause — unique to Python — runs only on success. Custom exceptions make errors self-documenting.

PYTHON — Basic
try:
    result = 10 / 0
except ZeroDivisionError:
    print("Cannot divide by zero.")
finally:
    print("This always runs.")  # cleanup goes here
PYTHON — Multiple except
# Multiple except — handle different exceptions differently:
def safe_parse_mark(text):
    try:
        mark = float(text)
        if not (0 <= mark <= 100):
            raise ValueError(f"Mark {mark} is outside valid range 0-100")
        return mark
    except ValueError as e:
        print(f"Invalid mark input: {e}")
        return None
    except TypeError as e:
        print(f"Wrong type: {e}")
        return None

print(safe_parse_mark("74.5"))  # 74.5
print(safe_parse_mark("abc"))   # ValueError
print(safe_parse_mark("150"))   # ValueError — out of range
print(safe_parse_mark(None))    # TypeError
PYTHON — else clause
# try/except/else/finally:
try:
    value = int("42")
except ValueError:
    print("Conversion failed.")
else:
    # Only runs if NO exception was raised
    print(f"Converted successfully: {value}")
finally:
    print("Always runs.")

# Output:
# Converted successfully: 42
# Always runs.

Custom Exceptions

PYTHON — Custom exceptions
class InsufficientFundsError(Exception):
    """Raised when a withdrawal exceeds the account balance."""
    def __init__(self, shortfall: float):
        self.shortfall = shortfall
        super().__init__(f"Insufficient funds. Short by R{shortfall:.2f}.")

class InvalidMarkError(ValueError):
    """Raised when a mark is outside the valid 0-100 range."""
    def __init__(self, mark):
        super().__init__(f"Mark must be 0-100. Got: {mark}")

# Using custom exceptions:
def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(amount - balance)
    return balance - amount

try:
    withdraw(500, 1200)
except InsufficientFundsError as e:
    print(e)
    print(f"You need R{e.shortfall:.2f} more.")

Context Managers and Exception Safety

PYTHON — Context managers
# The with statement is Python's try-finally for resources:
# This:
with open("file.txt") as f:
    data = f.read()

# Is equivalent to:
f = open("file.txt")
try:
    data = f.read()
finally:
    f.close()  # guaranteed even on exception

Practice Task

Your Turn

Write a LearnerDatabase class with custom exceptions DatabaseFullError (max 50 learners), LearnerNotFoundError, DuplicateIdError. Implement add_learner(), get_learner(id), remove_learner(id), update_mark(id, mark). Each method raises the appropriate custom exception. Test all exception paths in a main script, catching each and printing a helpful message.

Common Mistakes

  • Bare except: catches SystemExit and KeyboardInterrupt — always specify the exception type.
  • Empty except blocks hide bugs completely.
  • Raising a new exception inside except without raise ... from e loses the original traceback.
  • Using exceptions for normal control flow — they are expensive and for exceptional conditions.

Professional Tip

When you catch an exception and want to add context then re-raise, use: raise RuntimeError('Loading failed') from original_error. This preserves both the original and new exception in the traceback.

Mini Quiz

Which clause runs only when the try block completed without exception?