Exception Handling
try / except / else / finally structure. Python's exception hierarchy. Catching multiple exception types. Raising exceptions with raise. Creating custom exception classes. The EAFP principle. Using exceptions as control flow in real programs.
What Are Exceptions?
An exception is a signal that something went wrong at runtime. When Python hits an error it cannot handle (dividing by zero, missing file, wrong type), it raises an exception object and unwinds the call stack until something catches it — or the program crashes with a traceback.
Without exception handling, even one bad user input can crash an entire program. With it, you can detect errors, respond gracefully, log them, and keep running.
try / except / else / finally
The full try block has four clauses. You don't need all of them every time — but understanding each one is essential for writing production-quality code.
# ── Basic try/except ────────────────────────── def safe_divide(a, b): try: result = a / b except ZeroDivisionError: return None else: return result # only reached if no exception print(safe_divide(10, 2)) # 5.0 print(safe_divide(10, 0)) # None # ── Full try/except/else/finally ────────────── def load_data(path): f = None try: f = open(path, encoding="utf-8") data = f.read() except FileNotFoundError: print(f"❌ File not found: {path}") return None except PermissionError as e: print(f"🔒 Permission denied: {e}") return None else: print(f"✅ Loaded {len(data)} characters") return data finally: if f: f.close() # always close, even on error # ── Catching multiple exceptions ────────────── def parse_input(text): try: value = int(text.strip()) return value except ValueError: print(f"'{text}' is not a valid integer") except AttributeError: print("Input must be a string") return None print(parse_input("42")) # 42 print(parse_input("hello")) # 'hello' is not a valid integer → None print(parse_input(None)) # Input must be a string → None
except: — always catch specific exceptionsexcept: catches everything — including SystemExit and KeyboardInterrupt, which prevents Ctrl+C from working. At minimum use except Exception:. Better yet, name the specific exception you expect. Broad catches hide bugs instead of fixing them.Raising Exceptions & Custom Exceptions
You can raise exceptions yourself with the raise keyword — when input is invalid, a business rule is violated, or a state is impossible. Creating custom exception classes lets you give errors meaningful names and carry extra data.
# ── Raising built-in exceptions ─────────────── def set_age(age): if not isinstance(age, int): raise TypeError(f"Age must be int, got {type(age).__name__}") if age < 0 or age > 150: raise ValueError(f"Age {age} is out of valid range (0–150)") return age # ── re-raise — catch, log, then pass on ─────── try: set_age("twenty") except TypeError as e: print(f"Caught: {e}") raise # re-raise the same exception # ── Custom exception hierarchy ──────────────── class AppError(Exception): """Base class for all BitWithBite app errors.""" pass class ValidationError(AppError): """Raised when user input fails validation.""" def __init__(self, field, message): self.field = field self.message = message super().__init__(f"[{field}] {message}") class DatabaseError(AppError): """Raised on database connection or query failure.""" def __init__(self, query, cause): self.query = query super().__init__(f"DB error on query '{query}': {cause}") def register_student(name, age, gpa): if not name.strip(): raise ValidationError("name", "Name cannot be blank") if not (0 <= gpa <= 4.0): raise ValidationError("gpa", f"GPA {gpa} not in 0.0–4.0") print(f"✅ Registered: {name}, age {age}, GPA {gpa}") try: register_student("Ali", 20, 5.2) except ValidationError as e: print(f"❌ Validation failed: {e}") print(f" Field: {e.field}") except AppError as e: print(f"❌ App error: {e}")
if key in d: return d[key], write try: return d[key] except KeyError: .... This is faster (one lookup not two), more idiomatic, and handles race conditions better. It's called the EAFP principle and is Python's preferred style.Context Managers & __enter__ / __exit__
The with statement you've been using for files is powered by the context manager protocol. Any class implementing __enter__ and __exit__ can be used with with. __exit__ receives exception info and can suppress the exception by returning True.
import time class Timer: """Context manager that times a code block.""" def __enter__(self): self.start = time.perf_counter() return self # bound to 'as t' def __exit__(self, exc_type, exc_val, exc_tb): self.elapsed = time.perf_counter() - self.start print(f"⏱️ Elapsed: {self.elapsed:.4f}s") return False # don't suppress exceptions with Timer() as t: total = sum(range(1_000_000)) # ⏱️ Elapsed: 0.0312s # ── contextlib.contextmanager — simpler decorator approach ── from contextlib import contextmanager @contextmanager def managed_resource(name): print(f"Opening {name}") try: yield name # 'with' body runs here except Exception as e: print(f"Error during {name}: {e}") finally: print(f"Closing {name}") with managed_resource("database") as r: print(f"Using {r}...") # Opening database # Using database... # Closing database
contextlib.suppress() to silently ignore specific errorsfrom contextlib import suppresswith suppress(FileNotFoundError): os.remove("temp.txt")This is cleaner than a try/except pass block when you genuinely want to ignore a specific error. Never use it to suppress exceptions you haven't thought about.
else block in a try statement execute?except: clause?finally block guarantee?field attribute?1. Custom exceptions:
ValidationError(field, message), AgeRangeError(age) inheriting from ValidationError2. A
validate_name(name) function — raises ValidationError if blank or too short (<2 chars)3. A
validate_age(age_str) — raises ValueError if not a number, AgeRangeError if outside 5–1204. A
validate_email(email) — raises ValidationError if no "@" or no "." after "@"5. A
get_input(prompt, validator) helper that loops until valid input (max 3 tries, then raises ValidationError)6. A
Timer context manager wrapping the whole registration to show how long it took
💡 Show hints
- AgeRangeError:
class AgeRangeError(ValidationError): def __init__(self, age): super().__init__("age", f"Age {age} out of range 5-120") - get_input loop:
for attempt in range(1, max_tries+1): try: val = input(...); validator(val); return val except ValidationError as e: print(e) - Email check:
if "@" not in email or "." not in email.split("@")[1]: raise ValidationError(...) - Use Timer as context manager around the registration block
import time # ── Custom exceptions ───────────────────────── class ValidationError(Exception): def __init__(self, field, msg): self.field = field; super().__init__(f"[{field}] {msg}") class AgeRangeError(ValidationError): def __init__(self, age): super().__init__("age", f"Age {age} not in 5–120") # ── Validators ──────────────────────────────── def validate_name(name): if len(name.strip()) < 2: raise ValidationError("name", "At least 2 characters required") return name.strip().title() def validate_age(s): try: age = int(s) except ValueError: raise ValidationError("age", "Must be a whole number") if not (5 <= age <= 120): raise AgeRangeError(age) return age def validate_email(email): parts = email.split("@") if len(parts) != 2 or "." not in parts[1]: raise ValidationError("email", "Invalid email format") return email.lower() # ── Retry helper ────────────────────────────── def get_input(prompt, validator, max_tries=3): for attempt in range(1, max_tries+1): try: raw = input(f" {prompt}: ") return validator(raw) except ValidationError as e: print(f" ⚠️ {e} ({max_tries-attempt} tries left)") raise ValidationError("input", "Too many failed attempts") # ── Timer context manager ───────────────────── class Timer: def __enter__(self): self.t0 = time.time(); return self def __exit__(self, *_): print(f"\n ⏱️ Registration took {time.time()-self.t0:.2f}s"); return False # ── Main ────────────────────────────────────── print("═" * 40) print(" 🎓 STUDENT REGISTRATION") print("═" * 40) try: with Timer(): name = get_input("Name", validate_name) age = get_input("Age", validate_age) email = get_input("Email", validate_email) print(f"\n ✅ Registered: {name}, {age}, {email}") except ValidationError as e: print(f"\n ❌ Registration failed: {e}")