Excellent.
This is PHASE 8 — Exceptions & Error Handling, taught at production-grade + senior interview depth.
Most developers “catch exceptions”.
Very few understand exception hierarchy, propagation, chaining, cost, design patterns, and failure semantics.
After this phase, your error handling will look intentional, safe, and professional.
🚨 PHASE 8 — EXCEPTIONS & ERROR HANDLING (DEEP)
8.1 WHAT AN EXCEPTION REALLY IS (INTERNAL VIEW)
An exception is:
- A runtime object
- Raised when execution cannot continue normally
- Propagated up the call stack
- Stops execution unless handled
raise ValueError("Invalid input")
Internally:
- Exception object created
- Stack unwound
- Nearest matching handler searched
📌 Interview line
Exceptions are control-flow interruptions, not return values.
8.2 EXCEPTION HIERARCHY (YOU MUST KNOW THIS)
All exceptions inherit from:
BaseException
├── Exception
│ ├── ValueError
│ ├── TypeError
│ ├── KeyError
│ ├── IndexError
│ └── RuntimeError
├── SystemExit
├── KeyboardInterrupt
└── GeneratorExit
🔴 CRITICAL RULE
❌ Never catch BaseException
except BaseException:
pass # ❌ VERY BAD
Why?
- Swallows
KeyboardInterrupt,SystemExit
✅ Always catch Exception
8.3 try / except / else / finally — FULL SEMANTICS
try:
risky()
except ValueError:
handle()
else:
success()
finally:
cleanup()
Execution rules
except→ only if exception occurselse→ only if NO exceptionfinally→ ALWAYS executes
📌 else avoids accidentally catching errors you didn’t intend to handle.
8.4 MULTIPLE EXCEPT BLOCKS (ORDER MATTERS)
try:
...
except ValueError:
...
except Exception:
...
📌 Most specific first, generic last
❌ Wrong:
except Exception:
except ValueError: # unreachable
8.5 CATCHING MULTIPLE EXCEPTIONS
except (ValueError, TypeError) as e:
...
Use when:
- Same recovery logic
- Same severity
8.6 EXCEPTION PROPAGATION (STACK UNWINDING)
def a():
b()
def b():
c()
def c():
raise ValueError("boom")
a()
If not caught:
- Exception bubbles up
- Program terminates
📌 Interview insight
Python unwinds the stack frame-by-frame until a handler is found.
8.7 RE-RAISING EXCEPTIONS (IMPORTANT)
try:
risky()
except ValueError:
log()
raise
✔ Preserves original traceback
❌ raise e loses context
8.8 EXCEPTION CHAINING (raise from)
try:
int("abc")
except ValueError as e:
raise RuntimeError("Conversion failed") from e
Why this matters:
- Keeps root cause
- Essential in layered systems
📌 Interview line
Exception chaining preserves causal context across layers.
8.9 SUPPRESSING CONTEXT (from None)
raise RuntimeError("failed") from None
Use when:
- Internal error is irrelevant
- You want clean API errors
⚠️ Use sparingly.
8.10 CUSTOM EXCEPTIONS (DESIGN PROPERLY)
❌ Bad
raise Exception("something went wrong")
✅ Good
class ConfigError(Exception):
pass
Why?
- Semantic meaning
- Targeted catching
- Cleaner APIs
📌 Rule
Create domain-specific exceptions.
8.11 EXCEPTION DESIGN HIERARCHY
class AppError(Exception): pass
class ValidationError(AppError): pass
class AuthError(AppError): pass
Benefits:
✔ Catch all app errors
✔ Handle subtypes differently
8.12 WHEN TO RAISE VS RETURN ERROR CODES
Pythonic rule
- Exceptional cases → raise
- Expected conditions → return
❌ Bad:
return -1
✅ Good:
raise FileNotFoundError(path)
📌 Interview killer line
Errors should not masquerade as valid values.
8.13 COST OF EXCEPTIONS (PERFORMANCE REALITY)
- Raising exceptions is expensive
- Catching is cheap
- Do NOT use exceptions for normal control flow
❌ Bad:
try:
x = d[k]
except KeyError:
...
✅ Better:
if k in d:
📌 Except is for exceptional, not frequent paths.
8.14 finally — GUARANTEED CLEANUP
f = open("x.txt")
try:
process(f)
finally:
f.close()
Even if:
- Exception raised
returnexecuted
8.15 CONTEXT MANAGERS (with) — EXCEPTION SAFE
with open("x.txt") as f:
process(f)
Internally:
__enter____exit__
__exit__ handles exceptions.
8.16 WRITING YOUR OWN CONTEXT MANAGER
from contextlib import contextmanager
@contextmanager
def managed():
print("enter")
try:
yield
finally:
print("exit")
Used for:
- Resource management
- Transactions
- Locks
8.17 EXCEPTIONS IN THREADS / ASYNC (IMPORTANT)
Threads
- Exceptions do not propagate to main thread
- Must be captured manually
Async
- Exceptions propagate via
await - Unhandled → task failure
📌 Interview trap many miss.
8.18 LOGGING VS RAISING (PRODUCTION RULE)
❌ Bad
log.error(e)
raise e
Why?
- Duplicate logs
✅ Good
- Log at boundary
- Raise internally
- Handle at top level
8.19 ANTI-PATTERNS (INTERVIEW FAVORITES)
❌ Bare except
except:
pass
❌ Swallowing errors
except Exception:
return None
❌ Over-catching
except Exception:
retry_forever()
8.20 TOP-LEVEL ERROR HANDLING PATTERN
def main():
try:
run()
except AppError as e:
log.error(e)
sys.exit(1)
📌 Clean separation:
- Inner layers → raise
- Outer layer → log + exit
🧪 PRACTICE (INTERVIEW-LEVEL)
Q1 — What’s wrong here?
try:
risky()
except:
pass
Q2 — Why is this better?
try:
risky()
except ValueError as e:
raise AppError("failed") from e
Q3 — When should you NOT use exceptions?
Q4 — Design custom exceptions for:
- Config errors
- Validation errors
- External service failures
Q5 — Explain else in try/except.
🎯 INTERVIEW CHECKPOINT (CRITICAL)
You must now confidently explain:
✅ Exception hierarchy
✅ Why not to catch BaseException
✅ Propagation & stack unwinding
✅ raise vs raise e
✅ Exception chaining
✅ Custom exception design
✅ Performance cost of exceptions
✅ Context managers
✅ Logging vs raising
If you can do this cleanly → you handle failures like a senior engineer.
🚀 NEXT PHASE (STRICT CONTINUATION)
Reply with ONLY ONE number:
1 → PHASE 9: Files, JSON, CSV, Serialization (deep, production-grade)
2 → 25-question exceptions interview drill (brutal)
We continue A→Z without deviation.