Modules Data And Errors
Exception Handling
An exception is Python's way of signaling that something went wrong during execution — an error that, if unhandled, stops your program immediately.
Jr Codex Python Notes
Level: Intermediate Prerequisites: Chapter 3 Time to complete: ~25 minutes
Table of Contents
- What is an Exception?
- try / except — The Basics
- Catching Specific Exceptions
- The Exception Hierarchy
elseandfinally- Accessing Exception Details
- Raising Exceptions Yourself
- Common Pitfalls
- Summary & Next Steps
1. What is an Exception?
An exception is Python's way of signaling that something went wrong during execution — an error that, if unhandled, stops your program immediately.
numbers = [1, 2, 3]
print(numbers[10]) # IndexError: list index out of range
print("this line never runs — the program already crashed")Uncaught Exception → Program Crashes
─────────────────────────────────────────
Traceback (most recent call last):
File "script.py", line 2, in <module>
print(numbers[10])
IndexError: list index out of range
─────────────────────────────────────────
Exception handling lets you anticipate what might go wrong and respond gracefully instead of crashing.
2. try / except — The Basics
try:
numbers = [1, 2, 3]
print(numbers[10])
except IndexError:
print("That index doesn't exist!")
print("Program continues running") # ✓ this DOES run nowtry/except Flow
─────────────────────────────────────────
try:
risky_code() ← if this raises an exception...
except SomeError:
handle_it() ← ...control jumps here instead of crashing
─────────────────────────────────────────
# Real-world example: user input that might not be a valid number
def get_age():
user_input = input("Enter your age: ")
try:
age = int(user_input)
return age
except ValueError:
print("That's not a valid number!")
return None3. Catching Specific Exceptions
Always catch the most specific exception type you expect — a bare except: (or except Exception:) hides bugs you didn't anticipate, making debugging much harder.
def divide(a, b):
try:
result = a / b
return result
except ZeroDivisionError:
print("Cannot divide by zero!")
return None
except TypeError:
print("Both arguments must be numbers!")
return None
print(divide(10, 0)) # "Cannot divide by zero!" → None
print(divide(10, "2")) # "Both arguments must be numbers!" → None# Catching multiple exception types with one handler
try:
value = int(input("Enter a number: "))
result = 100 / value
except (ValueError, ZeroDivisionError) as e:
print(f"Invalid input: {e}")Why not just use bare except:?
# Dangerous — catches EVERYTHING, including typos and KeyboardInterrupt (Ctrl+C)!
try:
resutl = compute_something() # typo: NameError
except:
print("Something went wrong") # you'll never see the real bug — a NameError got silently swallowed4. The Exception Hierarchy
Python exceptions form a class hierarchy (a preview of inheritance, covered fully in Module 4) — except matches the named class and any of its subclasses.
BaseException
└── Exception
├── ArithmeticError
│ └── ZeroDivisionError
├── LookupError
│ ├── IndexError (list index out of range)
│ └── KeyError (missing dict key)
├── ValueError (right type, invalid value — e.g. int("abc"))
├── TypeError (wrong type entirely)
├── FileNotFoundError
├── AttributeError (object has no such attribute/method)
└── ...
try:
result = 10 / 0
except ArithmeticError: # ZeroDivisionError IS-A ArithmeticError — this catches it
print("Some arithmetic error occurred")
try:
my_dict = {"a": 1}
print(my_dict["b"])
except Exception: # catches almost anything — usually too broad, but shown for illustration
print("Some exception occurred")Order matters — put more specific exceptions before more general ones, or the general one will swallow everything first:
try:
risky_operation()
except Exception: # ✗ this catches EVERYTHING first
print("Generic error")
except ValueError: # ✗ unreachable — Exception already caught it above!
print("Specific error")5. else and finally
def read_config(filename):
try:
with open(filename, "r") as file:
data = file.read()
except FileNotFoundError:
print(f"{filename} not found, using defaults")
data = "{}"
else:
print("File read successfully") # runs ONLY if no exception occurred
finally:
print("Done attempting to read config") # ALWAYS runs — success, failure, or return
return datatry / except / else / finally
─────────────────────────────────────────
try: → code that might fail
except: → runs ONLY if an exception occurred
else: → runs ONLY if NO exception occurred
finally: → ALWAYS runs, no matter what (cleanup code)
─────────────────────────────────────────
finally is the standard place for cleanup that must happen regardless of success or failure — closing a network connection, releasing a lock, etc. (though with, from Chapter 2, already handles this automatically for files).
6. Accessing Exception Details
try:
result = 10 / 0
except ZeroDivisionError as e:
print(f"Error message: {e}") # "division by zero"
print(f"Error type: {type(e).__name__}") # "ZeroDivisionError"
try:
data = {"name": "Alice"}
print(data["age"])
except KeyError as e:
print(f"Missing key: {e}") # "'age'" — the missing key itself7. Raising Exceptions Yourself
Use raise to signal an error condition in your own code — you don't have to wait for Python to raise one naturally.
def withdraw(balance, amount):
if amount <= 0:
raise ValueError("Withdrawal amount must be positive")
if amount > balance:
raise ValueError("Insufficient funds")
return balance - amount
try:
withdraw(100, -50)
except ValueError as e:
print(f"Transaction failed: {e}") # "Transaction failed: Withdrawal amount must be positive"Re-raising an Exception
Sometimes you want to log or react to an error, then let it propagate anyway:
def process_data(data):
try:
return 100 / data
except ZeroDivisionError:
print("Logging: division by zero occurred")
raise # re-raises the SAME exception, preserving the original traceback8. Common Pitfalls
# Pitfall 1: catching Exception too broadly, hiding real bugs
try:
result = complex_calculation()
except Exception: # swallows EVERYTHING, including typos and logic errors
result = 0
# Better — catch what you actually expect
try:
result = complex_calculation()
except (ValueError, ZeroDivisionError):
result = 0
# Pitfall 2: using exceptions for normal control flow (usually a code smell)
try:
value = my_dict["key"]
except KeyError:
value = None
# Better here — .get() is clearer AND avoids the overhead of an exception:
value = my_dict.get("key")
# Pitfall 3: silently swallowing errors with `pass`
try:
risky_operation()
except Exception:
pass # ✗ error vanishes with NO trace — a debugging nightmare later9. Summary & Next Steps
Key Takeaways
try/exceptcatches exceptions so your program can recover instead of crashing; always catch the specific exception type you expect.- Python's exceptions form a class hierarchy —
except Exception:catches almost everything, which is usually too broad for real code. elseruns only on success;finallyalways runs — useful for guaranteed cleanup.- Use
raiseto signal your own error conditions; use bareraise(no argument) inside anexceptblock to re-raise the original exception after logging/reacting to it.
Concept Check
- Why is a bare
except:considered bad practice? - What's the difference between the
elseandfinallyblocks in atrystatement? - When should you use
raise ValueError(...)versus just returningNone?
Next Chapter
→ Chapter 5: Custom Exceptions & Debugging
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index