Advanced Exception Handling
advanced25 minLearning objectives
- Produce meaningful error messages
- Write robust, user-friendly programs
Learn
AQA 4.1.1.9 — Advanced exception handling
Retrieval: the previous lesson handled one risky operation at a time, catching one specific exception type. Real programs are rarely that simple — a single function often has several genuinely different ways it can fail, each needing a genuinely different response.
Key vocabulary
- Exception hierarchy — Python's built-in exceptions are organised so more specific types (like
ValueError) are also instances of more general ones (likeException). - Multiple except clauses — a single
tryblock can be followed by severalexceptclauses, each catching a different exception type. - Re-raising — deliberately letting an exception continue upward (via
raise) after partially handling it, rather than swallowing it completely.
Understand — clause order is not a formality
When Python hits an exception inside try, it checks each except clause in the order they're written, top to bottom, and runs the first one that matches. Because more general exception types (like Exception) match almost anything, placing a general clause before a specific one means the specific clause can never run — the general one intercepts everything first. This is exactly why clause order needs to go from most specific to most general, not the other way round.
See it — validating a multi-field form, robustly
def register_student(name_input, age_input, email_input):
errors = []
try:
age = int(age_input)
if age < 11 or age > 19:
errors.append("Age must be between 11 and 19.")
except ValueError:
errors.append("Age must be a whole number.")
if not name_input.strip():
errors.append("Name cannot be blank.")
if "@" not in email_input:
errors.append("Email must contain an @ symbol.")
return errors
This function collects every problem with the submitted data, rather than stopping at the first one — genuinely more user-friendly than forcing a student to fix and resubmit one error at a time.
Ordering except clauses correctly
try:
value = int(scores[index])
except IndexError:
print("That position doesn't exist in the list.")
except ValueError:
print("That entry isn't a valid number.")
except Exception:
print("Something else went wrong.")
Here, the two specific exceptions are listed before the general Exception catch-all — if Exception were listed first, it would silently intercept both the IndexError and the ValueError cases, and neither of the more specific, more useful messages would ever print.
Debug it — diagnose, explain, fix, test, justify
def safe_lookup(prices, product_name):
try:
return prices[product_name]
except Exception:
print("Product not found.")
except KeyError:
return 0.0
The intention is: if product_name isn't a valid key, print a message and return a default price of 0.0. Testing it with a missing product name prints "Product not found." but then crashes with TypeError: 'NoneType' object is not subscriptable wherever the caller tries to use the returned value.
- Diagnose: which
exceptclause actually runs whenproduct_nameis missing fromprices? - Explain: why does the
KeyErrorclause never get a chance to run, even though it's the one written to match this exact situation? - Fix: reorder (or otherwise correct) the clauses so a missing product both prints the message and returns
0.0. - Test: confirm your fixed version returns
0.0(notNone) for a missing product, and the correct price for one that exists. - Justify: state the general rule this bug demonstrates about ordering
exceptclauses.
(KeyError is a more specific subtype of Exception, so the general "except Exception" clause (listed first) catches it before the KeyError clause below it ever gets a chance to run - and that general clause only prints a message, never returns anything, so the function implicitly returns None. Reordering so KeyError comes before the general Exception clause fixes it. General rule: except clauses must be ordered from most specific to most general.)
Common mistake
Using a bare except: (with no exception type at all) as a "safety net" at the end of a chain of specific clauses. This catches everything, including exceptions that have nothing to do with the situation being handled (a typo causing a NameError, for instance) — masking genuine bugs as if they were the expected failure case, exactly the "meaningful error messages" problem the previous lesson warned about, now compounded by clause ordering.
Check your understanding
A function has except ZeroDivisionError: and except ArithmeticError:, in that order, for the same try block (in Python, ZeroDivisionError is a subtype of ArithmeticError). Which order is correct, and why does it matter here specifically? (The order shown - ZeroDivisionError before the more general ArithmeticError - is correct, for the same reason as the worked examples: the more specific type must come first, or the general ArithmeticError clause would catch ZeroDivisionError cases too and the specific handler would never run.)
Challenge
Extend the register_student function above to also validate that the email contains at least one . after the @ symbol, collecting this as an additional error alongside any others found — without stopping at the first problem detected.
Looking ahead: the next lesson (Understanding Stack Frames) moves from handling failures gracefully to understanding exactly what a function call itself does in memory — the mechanism every exception handler in this lesson has been running on top of.