Skip to content

Latest commit

 

History

History
241 lines (162 loc) · 7.82 KB

File metadata and controls

241 lines (162 loc) · 7.82 KB

Speaker notes — 5. Exceptions (10:55–11:25)

This section is scheduled for 30 minutes (see course_timetable.md: 10:55–11:25).

Outcomes (say this upfront)

By the end of this section, participants should be able to:

  • Explain what an exception is and why they happen.
  • Use try/except for anticipated edge cases.
  • Add context by catching and re-raising exceptions with meaningful messages.
  • Validate inputs and raise a suitable exception early.
  • Use finally to ensure something always happens (e.g., cleanup, logging, timing).

Timing plan (30 minutes)

  • 10:55–11:00 (5 min) — What exceptions are + try/except syntax
  • 11:00–11:08 (8 min) — Edge cases: average([]) (handle empty input)
  • 11:08–11:16 (8 min) — Re-raise with context: parsed_max(...)
  • 11:16–11:20 (4 min) — Raise for invalid values: water_pressure(-5)
  • 11:20–11:23 (3 min)finally: always log timing in timed_calculation(...)
  • 11:23–11:25 (2 min) — Best-practice recap + set up the GPA exercise (start now or as a take-home)

Talk track (what to say)

1) Framing + syntax (10:55–11:00)

  • “In real data and real code, assumptions break: missing values, malformed strings, unexpected types, network/filesystem hiccups.”
  • “When Python cannot continue, it raises an exception. That’s not ‘bad’—it’s Python telling us something is inconsistent with the code’s expectations.”
  • “Our job is to decide:
    1. can we recover and keep going?
    2. or should we stop early, with a clear message?”

Point to the general syntax on the slide/cell:

try:
    statement()
except ExceptionType:
    handling_statement()

Say:

  • “We only catch exceptions we expect and know how to handle.”
  • “Catching everything (except Exception:) can hide bugs; we’ll prefer specific exception types where possible.”

Quick check-in question:

  • “What exception do you get if you divide by zero?” (Expect: ZeroDivisionError.)

2) Known edge case: empty input (11:00–11:08)

Open the average(numbers) example and run it.

What to say:

  • “This works for normal input, but look at average([]).”
  • “This is a great example of an edge case: the function’s logic is fine, but the input breaks an assumption (non-empty list).”

Ask:

  • “What should the average of an empty list be?”
    • “Sometimes there isn’t a single correct answer—pick what makes sense for your project. Here, we’ll return 0 because the notebook asks for that.”

Implementation options to mention:

  • Preferred here: explicit check (clearer than exceptions for control flow)
    • “If numbers is empty, return 0 early.”
  • Alternative: try/except ZeroDivisionError
    • “Works, but can be less clear than checking the condition we actually care about.”

Prompt participants:

  • “Take 60–90 seconds: edit the function so average([]) returns 0.”

If you need to show a minimal solution quickly:

def average(numbers):
    if not numbers:
        return 0
    return sum(numbers) / len(numbers)

3) Re-raise with context (11:08–11:16)

Move to parsed_max(values) and run it.

What to say:

  • “This function tries to parse every entry as a float and finds the maximum.”
  • “The second list contains a date string. float("2020-05-21") fails.”
  • “The default traceback tells you what failed, but not always enough about where in your data the problem is.”

Key message:

  • “When you catch an exception, add context (index, value) and then re-raise.”

Steer the solution:

  • Catch the specific error around the risky operation (float(value)), not around the whole loop.
  • Include the index and original value.
  • Use exception chaining: raise ... from exc.

Suggested approach to narrate (don’t have to type it all unless needed):

def parsed_max(values):
    max_value = None
    for i, value in enumerate(values):
        try:
            parsed_value = float(value)
        except ValueError as exc:
            raise ValueError(f"Could not parse value at index {i}: {value!r}") from exc

        if max_value is None or parsed_value > max_value:
            max_value = parsed_value
    return max_value

Quick teaching notes:

  • value!r shows quotes so you can spot whitespace.”
  • from exc keeps the original traceback attached, which is helpful for debugging.”

4) Raise for invalid values (11:16–11:20)

Go to water_pressure(depth_m).

What to say:

  • “Here the code runs, but depth_m = -5 is physically nonsensical.”
  • “This is where we should validate input and fail early with a clear exception.”

Ask:

  • “Where should validation live?”
    • “Inside the function: the unit of code that knows what a valid ‘depth’ is.”

Minimal fix to mention/show:

def water_pressure(depth_m):
    if depth_m < 0:
        raise ValueError(f"depth_m must be non-negative, got {depth_m}")
    g = 9.81
    density = 1000
    return density * g * depth_m

5) finally: always execute (11:20–11:23)

Go to timed_calculation(n).

What to say:

  • “The function prints timing at the end… but notice what happens when n < 0.”
  • “An exception happens before we print timing, so we lose information.”
  • finally is great for guaranteed logging/cleanup.”

Goal (as per the notebook):

  • “Modify the function so it always prints the elapsed time, even when it errors.”

Strategy to explain:

  • Put the calculation in a try.
  • Put timing/printing in finally.
  • Keep raising for invalid n, but still print the elapsed time.

One possible implementation:

import time

def timed_calculation(n):
    start_time = time.time()
    try:
        if n < 0:
            raise ValueError("n must be positive")
        result = sum(i**2 for i in range(n))
        return result
    finally:
        elapsed = time.time() - start_time
        print(f"Calculation took {elapsed:.6f} seconds")

Note to say:

  • finally runs whether we return or raise.”

6) Best practice recap + GPA exercise (11:23–11:25)

Use the “Best Practice” cell as your summary.

Say:

  • “First choice: handle the situation if there’s a sensible fallback.”
  • “Otherwise: fail early, validate near the source, and raise an exception with a meaningful message.”
  • “If you struggle to run your own script, others will struggle more—good error messages are a kindness.”

Then set up the final exercise:

Exercise: Grade Point Average Calculator

Read the tasks aloud:

  1. “Handle empty grade lists by returning 0.0.”
  2. “Add exception handling for non-numeric grades that includes the problematic grade and its position.”

If you have time:

  • “Take ~3 minutes to implement; I’ll walk around. Then we’ll compare approaches.”

If you’re short on time:

  • “Start this now; finish after the session as practice.”

Hints you can say (without giving everything away immediately):

  • “Use enumerate(grades) to get the position.”
  • “Guard against empty list before dividing.”
  • “If someone passes 'A', decide whether you want to skip, convert, or raise. The exercise asks for a meaningful error.”

A reference solution (keep as teacher-only):

def calculate_gpa(grades):
    if not grades:
        return 0.0

    total_points = 0.0
    for i, grade in enumerate(grades):
        try:
            total_points += float(grade)
        except (TypeError, ValueError) as exc:
            raise TypeError(f"Non-numeric grade at index {i}: {grade!r}") from exc

    return total_points / len(grades)