Learning Goals
3 minBy the end of this lesson you can:
- Wrap a risky operation in
try:and catch the failure inexcept:. - Read a Python traceback — the bottom line tells you the exception type.
- Explain why a bare
except:is dangerous, and what to do instead. - Choose between "look before you leap" (LBYL) and "easier to ask forgiveness" (EAFP).
Warm-Up · A Crash Course
5 minWhat happens if the user types banana into this prompt?
age = int(input("Your age: ")) print(f"Next year you'll be {age + 1}.")
Your age: banana
Traceback (most recent call last):
File "age.py", line 1, in <module>
age = int(input("Your age: "))
ValueError: invalid literal for int() with base 10: 'banana'The program crashes and your user sees a wall of red text. Today's fix:
try: age = int(input("Your age: ")) print(f"Next year you'll be {age + 1}.") except ValueError: print("Please type a number next time.")
The risky line is inside try. If it raises a ValueError, the except block runs instead. The program continues — no crash, no traceback.
An exception is Python's way of saying "something went wrong". Without try/except, exceptions end your program. With them, you handle the problem and keep going.
New Concept · Catching Crashes
14 minThe shape
try: # risky code except ExceptionType: # what to do if it goes wrong
If anything inside try raises an exception of the type listed in except, Python jumps to the except block. If nothing goes wrong, the except block is skipped entirely.
Reading a traceback
Every traceback follows the same shape. The last line is the most important.
Traceback (most recent call last):
File "thing.py", line 5, in <module>
n = int("twelve")
ValueError: invalid literal for int() with base 10: 'twelve'- Last line — exception type and message. This is what you put in your except clause.
- Middle lines — "call stack" — where in your code it blew up.
The most common exception types
Type Caused by
ValueError int("hi"), float("twelve")
TypeError "5" + 3, len(7)
ZeroDivisionError 1 / 0
IndexError list[10] on a 3-item list
KeyError d["missing"] on a dict
FileNotFoundError open("does-not-exist.txt")
PermissionError writing to a read-only file
KeyboardInterrupt user pressed Ctrl+C
StopIteration (advanced — iterating past the end)You don't need to memorise them — they appear in tracebacks all the time and you'll soon recognise the regulars.
The two main shapes
1 · Recover and continue. Catch, tell the user, loop again.
while True: try: n = int(input("Pick a number: ")) break except ValueError: print("Numbers only — try again.") print(f"You picked {n}.")
2 · Catch and fall back. If the risky thing fails, use a default.
try: with open("scores.txt") as f: scores = [int(line) for line in f] except FileNotFoundError: scores = [] # treat "no file" as "no scores yet" print(f"Loaded {len(scores)} scores.")
Don't do this · bare except
Writing except: with no type catches everything — including KeyboardInterrupt (Ctrl+C) and bugs in your own code. Your program ignores them silently and becomes very hard to debug.
# ❌ Don't write code like this try: do_stuff() except: pass # the silent killer — swallows EVERYTHING
Always name the exception you're actually expecting. The minimum acceptable improvement is except Exception as e: — at least that still lets KeyboardInterrupt through and gives you the error message in e.
Capturing the message · as e
Use except SomeError as e to get the actual exception object, which you can print or log.
try: age = int(input("Age: ")) except ValueError as e: print(f"Sorry — that didn't work: {e}")
For the input "abc", the print becomes Sorry — that didn't work: invalid literal for int() with base 10: 'abc'.
LBYL vs EAFP
Two philosophies, both legitimate:
# LBYL — Look Before You Leap text = input("Number: ") if text.isdigit(): n = int(text) else: print("Not a number.") # EAFP — Easier to Ask Forgiveness than Permission try: n = int(input("Number: ")) except ValueError: print("Not a number.")
Python folks lean EAFP. The check inside if is often a sloppy duplicate of what the function will do anyway — and easier to get wrong. Try the thing; catch the failure. That's the Pythonic way.
Worked Example · The Robust Age-Gate
12 minThe story
Build a tiny age-gate that asks for the user's age. It must survive any input — including blank strings, letters, negative numbers and silly large values — without crashing.
Save as age_gate.py:
# age_gate.py — bullet-proof age input while True: raw = input("Your age (1-120): ").strip() try: age = int(raw) except ValueError: print(" ! Not a number. Try again.") continue if age < 1 or age > 120: print(f" ! {age} is not a realistic age. Try again.") continue break print(f"\nThank you. You are {age} years old.") if age >= 13: print("You're old enough to use this service.") else: print("Sorry, this service is for 13 and up.")
Sample session
Your age (1-120): banana ! Not a number. Try again. Your age (1-120): ! Not a number. Try again. Your age (1-120): -7 ! -7 is not a realistic age. Try again. Your age (1-120): 12 Thank you. You are 12 years old. Sorry, this service is for 13 and up.
Read the diff
Two layers of validation. The try handles "the input wasn't a number at all". The if handles "it was a number but the value is wrong". Both continue back to the top of the loop. break only runs when both layers pass — clean control flow.
Try It Yourself
13 minAsk the user for two numbers and print a / b. Catch ZeroDivisionError if they pick zero for the divisor; catch ValueError if either input isn't a number.
Hint
try: a = float(input("a: ")) b = float(input("b: ")) print(f"a / b = {a / b}") except ValueError: print("Numbers only please.") except ZeroDivisionError: print("Can't divide by zero.")
Two except blocks, one per failure mode. We'll dive deeper into multi-except in PY-L2-27.
Read numbers.txt (one number per line). Print the sum. If the file doesn't exist, print 0. If a line isn't a number, skip it.
Hint
total = 0 try: with open("numbers.txt") as f: for line in f: try: total += int(line.strip()) except ValueError: pass # skip non-numbers silently except FileNotFoundError: pass # leave total at 0 print(f"Total: {total}")
An inner try inside a loop — perfect for "skip the bad rows, keep the good ones".
Build a tiny phone book lookup with safe error handling — but use try/except KeyError instead of .get(). (You've done it with .get() before — today, try EAFP.)
Hint
book = {"Aisyah": "012-3456", "Wei Jie": "017-9921"} name = input("Look up: ") try: print(book[name]) except KeyError: print("Not in the book.")
Compare to the LBYL version with if name in book:. Both work; the EAFP version makes the "normal case" the simple code at the top.
Mini-Challenge · The Hardened Calculator
8 minBuild calc.py, a tiny one-line calculator. The user types a OP b where OP is + - * /. Print the result. Catch every bad-input case you can think of.
Failure modes to handle:
- Wrong number of tokens (less than or more than 3 after
.split()). - First or third token isn't a number.
- Middle token isn't one of
+ - * /. - Division by zero.
For every failure mode, print a friendly message and loop again.
Show one possible solution
# calc.py — a one-line calculator that survives anything while True: line = input("\nCalc (or 'q'): ").strip() if line == "q": break parts = line.split() if len(parts) != 3: print(" ! Type three things, like '3 + 4'.") continue try: a = float(parts[0]) b = float(parts[2]) except ValueError: print(" ! First and third must be numbers.") continue op = parts[1] if op not in "+-*/": print(" ! Operator must be + - * /.") continue try: if op == "+": result = a + b elif op == "-": result = a - b elif op == "*": result = a * b else: result = a / b except ZeroDivisionError: print(" ! Can't divide by zero.") continue print(f"= {result}")
Non-negotiables: a while True menu loop with a quit option, a length check, two try/except blocks for parsing and division, and a polite message for every failure mode. Notice the early-continue pattern — each failure case ends with continue so the next iteration of the loop restarts cleanly.
Recap
3 minExceptions are Python's "something went wrong" signal. Wrap a risky operation in try: and catch a specific failure with except SomeError:. The last line of a traceback always tells you what type of exception was raised — that's the name you put in your except. Never use bare except: — it swallows even Ctrl+C and your own bugs. Prefer EAFP: try the thing, catch the failure.
Vocabulary Card
- exception
- An error condition that interrupts normal flow until something catches it.
- traceback
- The stack trace Python prints when an exception isn't caught. Last line = the type.
- try / except
- The block-pair that catches an exception and runs an alternative.
- bare except
- Writing
except:with no type. Bad practice — catches everything, including Ctrl+C. - LBYL / EAFP
- Look Before You Leap (check first) / Easier to Ask Forgiveness than Permission (try first, catch failures). Python prefers EAFP.
Homework
4 minRe-open your favourite game from earlier in Level 2 (Number Guesser, Hangman, Wordle-Lite). Find every place you call int() on user input. Wrap each one in a try/except ValueError so the game never crashes on a bad number. Hand in the updated game file.
Stretch. Find every open() in your games (high-score saves etc.) and wrap them in try/except FileNotFoundError for first-run safety. Test by deleting the score file and running the game — it should start cleanly, not crash.
Sample · safe_int helper
def safe_int(prompt, lo=None, hi=None): while True: try: n = int(input(prompt)) except ValueError: print(" ! Type a whole number.") continue if lo is not None and n < lo: print(f" ! At least {lo} please.") continue if hi is not None and n > hi: print(f" ! At most {hi} please.") continue return n # Usage replaces every int(input(...)) call: guess = safe_int("Guess 1-100: ", 1, 100)
Build the helper once; use it everywhere. That's how a tiny try/except turns into infrastructure that protects every input in every game you write from now on.