Learning Goals
3 minBy the end of this lesson you can:
- Stack multiple
exceptblocks under onetry— Python jumps to the first matching one. - Catch several types in one block with a tuple:
except (ValueError, TypeError):. - Use
raise SomeError(message)to signal your own problems. - Know the broad family tree of built-in exceptions so you can pick the right one.
Warm-Up · Two Ways to Fail
5 minThis little snippet has two failure modes. Can you spot both?
raw = input("How many sweets? ") sweets = int(raw) each = 100 / sweets print(f"Each person gets {each} grams.")
Reveal the failure modes
1 · ValueError if the user types something non-numeric like two.
2 · ZeroDivisionError if the user types 0.
Two different problems. We'd like to react to each with a different message.
try: raw = input("How many sweets? ") sweets = int(raw) each = 100 / sweets print(f"Each person gets {each} grams.") except ValueError: print("Please type a number.") except ZeroDivisionError: print("Hmm, can't share zero sweets.")
Different problems deserve different responses. Stack multiple except blocks; Python jumps to the first one whose type matches.
New Concept · Three Patterns + raise
14 minPattern 1 · One except per type
try: risky() except ValueError: print("Bad value.") except FileNotFoundError: print("Missing file.") except ZeroDivisionError: print("Zero divide.")
Stack as many as you need. The blocks are checked in order; the first one whose type matches the exception runs, and the rest are skipped.
Pattern 2 · Several types in one block
If the response would be identical, group them with a tuple.
try: n = int(input("Number: ")) result = 100 / n except (ValueError, ZeroDivisionError): print("Couldn't compute. Try a non-zero number.")
Note the round brackets — that's a tuple of exception types. Don't write except ValueError, ZeroDivisionError: (no brackets) — that's old Python 2 syntax and will fail in Python 3.
Pattern 3 · The catch-all at the bottom
Use Exception as a generic fallback for "anything else I forgot". Always put it last; otherwise it grabs everything before the specific blocks get a chance.
try: risky() except ValueError: print("Bad value.") except FileNotFoundError: print("Missing file.") except Exception as e: print(f"Something else went wrong: {e}")
Catching Exception still lets KeyboardInterrupt through (good) — unlike the bare except: from yesterday.
The order matters · subclasses come first
Some exception types inherit from others. FileNotFoundError is a kind of OSError. If you put OSError first, it'll grab everything and the FileNotFoundError block never runs.
# Right try: open("missing.txt") except FileNotFoundError: print("Not found") except OSError: print("Other disk problem") # Wrong — FileNotFoundError block is unreachable try: open("missing.txt") except OSError: print("Other disk problem") except FileNotFoundError: print("Not found") # never runs!
Rule: specific first, general last.
The exception family tree (the bits you'll meet)
BaseException
└─ Exception
├─ ArithmeticError
│ └─ ZeroDivisionError
├─ LookupError
│ ├─ IndexError
│ └─ KeyError
├─ ValueError
├─ TypeError
└─ OSError
├─ FileNotFoundError
├─ PermissionError
└─ IsADirectoryErrorYou don't need to memorise it — but knowing that FileNotFoundError is a kind of OSError stops the "why isn't my catch running?" bug.
Raising your own
To signal "something's wrong" from your own code, use raise. Pick the right type and write a clear message.
def withdraw(balance, amount): if amount <= 0: raise ValueError(f"Amount must be positive: {amount}") if amount > balance: raise ValueError(f"Insufficient funds: balance is {balance}, want {amount}") return balance - amount try: new_balance = withdraw(100, -10) except ValueError as e: print(f"Withdrawal failed: {e}") # → Withdrawal failed: Amount must be positive: -10
Two things to spot. (1) raise SomeError("message") stops the function and the exception bubbles up. (2) Callers catch it with the usual try/except.
Sometimes you want to log a problem and let it keep travelling up:
try: risky() except ValueError as e: log(f"got a ValueError: {e}") raise # ← re-raise — don't swallow it
A bare raise inside an except re-throws the same exception with the original traceback intact. Powerful.
Worked Example · A Bullet-Proof File Reader
12 minThe story
Build safe_loader.py. It opens a numeric file and computes the average. There are three failure modes; we react differently to each.
- File missing — print a polite message, exit clean.
- File present but blank — "no data", exit clean.
- File has at least one non-numeric line — skip that line, keep going.
Code
# safe_loader.py — file loading with three handled failure modes def load_numbers(path): numbers = [] skipped = 0 try: with open(path) as f: for line in f: line = line.strip() if line == "": continue try: numbers.append(float(line)) except ValueError: skipped += 1 except FileNotFoundError: raise # let the caller decide what to do except PermissionError as e: raise RuntimeError(f"Can't read {path}: {e}") return numbers, skipped try: nums, bad = load_numbers("temperatures.txt") except FileNotFoundError: print("No temperatures file yet — nothing to average.") raise SystemExit except RuntimeError as e: print(f"Couldn't read the file: {e}") raise SystemExit if len(nums) == 0: print("File is empty.") raise SystemExit avg = sum(nums) / len(nums) print(f"{len(nums)} numbers loaded ({bad} bad lines skipped). Average: {avg:.2f}")
Sample outputs (depending on the file)
# temperatures.txt missing: No temperatures file yet — nothing to average. # temperatures.txt = "31\n32\nthirty\n29\n": 3 numbers loaded (1 bad lines skipped). Average: 30.67 # temperatures.txt is empty: File is empty.
Read the diff
Two layers of try/except — one inside the loader (catches per-line problems, decides to re-raise the file-level ones) and one outside (catches the file-level problems and exits clean). That separation is the right structure for any file-handling code: low-level catches the small stuff and lets the big stuff bubble; the caller's catch makes the big stuff polite.
Try It Yourself
13 minWrite a function safe_divide(text) that takes a string like "6 / 2" and returns the quotient — but handles three failure modes with three different messages.
Hint
def safe_divide(text): try: a, _, b = text.split() return float(a) / float(b) except ValueError: return "Format must be 'a / b' with numbers." except ZeroDivisionError: return "Can't divide by zero." print(safe_divide("6 / 2")) # → 3.0 print(safe_divide("six / 2")) # → Format must be 'a / b' with numbers. print(safe_divide("6 / 0")) # → Can't divide by zero.
Note that the missing-third-token case also raises a ValueError (from unpacking) — same except, same message. Two failure modes, one catch.
Modify your safe_divide so any of ValueError, TypeError or IndexError all give the same message. Use one except block with a tuple.
Hint
try: ... except (ValueError, TypeError, IndexError): return "Format must be 'a / b' with numbers." except ZeroDivisionError: return "Can't divide by zero."
Write charge_credit_card(amount). If amount is negative, raise ValueError. If it's more than 5000, raise RuntimeError ("daily limit"). Test with three calls.
Hint
def charge_credit_card(amount): if amount < 0: raise ValueError(f"Negative amount: {amount}") if amount > 5000: raise RuntimeError(f"Daily limit exceeded: {amount}") print(f"Charged RM {amount:.2f}") for amt in [123, -5, 9000]: try: charge_credit_card(amt) except ValueError as e: print(f"❌ ValueError: {e}") except RuntimeError as e: print(f"❌ RuntimeError: {e}")
The function doesn't catch anything — it just raises. The caller decides how to react. That's healthy: low-level code reports problems; high-level code chooses the response.
Mini-Challenge · Robust CSV Loader
8 minBuild load_grades.py. The file grades.csv has lines like:
Aisyah,92 Wei Jie,87 Priya,1000 ← bug! out of range Iman,nine ← bug! not a number Aizat, Hafiz,73
Build load_grades.py that:
- Reads the file line by line.
- For each line: split on comma, expect two pieces, convert second to int, validate 0-100.
- Use
raise ValueError(...)from your own validator when the score is out of range. - Catch
ValueErrorat the loop level — print the bad line and skip it. - Print all valid grades at the end and the count of skipped lines.
Stretch goal. Also handle FileNotFoundError with a fallback message, and use a tuple-except to catch both unpacking errors and conversion errors with one branch.
Show one possible solution
# load_grades.py — robust grade loader def parse_line(line): parts = line.strip().split(",") if len(parts) != 2: raise ValueError(f"Wrong number of fields: {line!r}") name = parts[0].strip() if name == "": raise ValueError(f"Missing name in: {line!r}") try: score = int(parts[1].strip()) except ValueError: raise ValueError(f"Score not an integer: {parts[1]!r}") if score < 0 or score > 100: raise ValueError(f"Score out of range (0-100): {score}") return name, score grades = [] skipped = 0 try: with open("grades.csv") as f: for line in f: if line.strip() == "": continue try: grades.append(parse_line(line)) except ValueError as e: print(f" ! skipped — {e}") skipped += 1 except FileNotFoundError: print("grades.csv not found — nothing to load.") raise SystemExit print(f"\nLoaded {len(grades)} grades (skipped {skipped}).") for name, score in grades: print(f" {name:<12} {score}")
Non-negotiables: a parse_line function that raises ValueError with descriptive messages, a loop that catches the error and reports without crashing, a file-level FileNotFoundError at the outer try, and a polished summary at the end.
Recap
3 minMultiple except blocks let you react differently to different failure modes. Group with a tuple when responses match. Order specifically-to-generally — a subclass before its parent. Use raise SomeError(message) to signal a problem from your own code; a bare raise inside an except re-throws the current exception. Catch Exception only as a last-resort fallback, never bare except:.
Vocabulary Card
- multiple except
- Several
exceptblocks under onetry. First matching type runs. - tuple except
except (A, B, C):— same handler for several types.- raise
- Manually trigger an exception from your own code.
- re-raise
- A bare
raiseinside an except — pass the exception on up the call stack. - exception hierarchy
- The family tree of built-in exceptions. Subclasses must be caught before parents.
Homework
4 minBuild safe_inputs.py — a small library of input helpers that wrap int(), float() and open() in proper exception handling.
Your file must include:
ask_int(prompt, lo=None, hi=None)— loop until the user gives a valid int in range.ask_float(prompt)— loop until they give a valid float.ask_yes_no(prompt)— loop until they give "y", "yes", "n" or "no". ReturnTrue/False.safe_open(path, mode="r")— return the file or print a friendly error and exit cleanly.
Stretch. At the bottom, write a 10-line demo that uses every helper.
Sample · safe_inputs.py
# safe_inputs.py — bullet-proof input helpers def ask_int(prompt, lo=None, hi=None): while True: try: n = int(input(prompt)) except ValueError: print(" ! Whole numbers only.") continue if lo is not None and n < lo: print(f" ! Must be at least {lo}.") continue if hi is not None and n > hi: print(f" ! Must be at most {hi}.") continue return n def ask_float(prompt): while True: try: return float(input(prompt)) except ValueError: print(" ! Numbers only (decimals OK).") YES = {"y", "yes"} NO = {"n", "no"} def ask_yes_no(prompt): while True: s = input(prompt).strip().lower() if s in YES: return True if s in NO: return False print(" ! Please answer yes or no.") def safe_open(path, mode="r"): try: return open(path, mode, encoding="utf-8") except FileNotFoundError: print(f"File not found: {path}") raise SystemExit except PermissionError: print(f"Permission denied: {path}") raise SystemExit if __name__ == "__main__": name = input("Name: ") age = ask_int("Age (1-120): ", 1, 120) rate = ask_float("Rate: ") if ask_yes_no("Save? (y/n) "): with safe_open("profile.txt", "w") as f: f.write(f"{name},{age},{rate}\n") print("Saved.")
Non-negotiables: four helpers, each loops until valid, each names the exact exception it's catching. The if __name__ == "__main__": at the bottom is a sneak peek at modules — we'll cover that properly in PY-L2-31.