This section is scheduled for 30 minutes (see course_timetable.md: 10:55–11:25).
By the end of this section, participants should be able to:
- Explain what an exception is and why they happen.
- Use
try/exceptfor anticipated edge cases. - Add context by catching and re-raising exceptions with meaningful messages.
- Validate inputs and
raisea suitable exception early. - Use
finallyto ensure something always happens (e.g., cleanup, logging, timing).
- 10:55–11:00 (5 min) — What exceptions are +
try/exceptsyntax - 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 intimed_calculation(...) - 11:23–11:25 (2 min) — Best-practice recap + set up the GPA exercise (start now or as a take-home)
- “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:
- can we recover and keep going?
- 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.)
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
0because the notebook asks for that.”
- “Sometimes there isn’t a single correct answer—pick what makes sense for your project. Here, we’ll return
Implementation options to mention:
- Preferred here: explicit check (clearer than exceptions for control flow)
- “If
numbersis empty, return0early.”
- “If
- 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([])returns0.”
If you need to show a minimal solution quickly:
def average(numbers):
if not numbers:
return 0
return sum(numbers) / len(numbers)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_valueQuick teaching notes:
- “
value!rshows quotes so you can spot whitespace.” - “
from exckeeps the original traceback attached, which is helpful for debugging.”
Go to water_pressure(depth_m).
What to say:
- “Here the code runs, but
depth_m = -5is 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_mGo 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.”
- “
finallyis 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:
- “
finallyruns whether wereturnorraise.”
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:
Read the tasks aloud:
- “Handle empty grade lists by returning
0.0.” - “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)