Exception Handling¶
When something goes wrong at runtime — file missing, network down, divide by zero — Python raises an exception. If you don't handle it, your program crashes.
A crashing example¶
The program stops at the error. Everything after is unreachable.
try / except — catch and recover¶
try:
x = 10
y = 0
print(x / y)
except ZeroDivisionError:
print("Can't divide by zero!")
print("Program continues happily.")
try:block — risky code.except SomeError:— what to do if that error happens.
Catching multiple errors¶
def safe_divide(a, b):
try:
return int(a) / int(b)
except ZeroDivisionError:
return "Error: division by zero"
except ValueError:
return "Error: inputs must be integers"
except Exception as e:
# Catch-all — keep last
return f"Unknown error: {e}"
print(safe_divide(10, 2)) # 5.0
print(safe_divide(10, 0)) # division by zero
print(safe_divide("a", "b")) # ValueError → "inputs must be integers"
You can catch several in one line as a tuple:
else and finally¶
else:runs if no exception was raised.finally:runs either way — for cleanup.
def read_number(text):
try:
n = int(text)
except ValueError:
print(f"'{text}' is not a number")
else:
print(f"Got number: {n}")
finally:
print("done\n")
read_number("42")
read_number("hello")
finally is perfect for closing files, releasing locks, etc. — but using with (context managers) is usually cleaner.
Common built-in exceptions¶
| Exception | When it happens |
|---|---|
ZeroDivisionError |
Dividing by zero |
ValueError |
Right type, wrong value (int("abc")) |
TypeError |
Wrong type ("a" + 5) |
KeyError |
Dict key missing (d["missing"]) |
IndexError |
List index out of range |
FileNotFoundError |
Trying to open a file that doesn't exist |
PermissionError |
OS denied access |
AttributeError |
obj.missing_method |
ImportError / ModuleNotFoundError |
import failed |
NameError |
Used a variable not defined |
StopIteration |
Iterator exhausted |
RecursionError |
Stack overflow |
RuntimeError |
Generic catch-all for misc problems |
Examples:
errors_demo = [
("ValueError", lambda: int("hello")),
("TypeError", lambda: "a" + 5),
("KeyError", lambda: {"a": 1}["b"]),
("IndexError", lambda: [1, 2, 3][10]),
("AttributeError", lambda: "hello".not_a_method()),
]
for name, fn in errors_demo:
try:
fn()
except Exception as e:
print(f"{name}: {type(e).__name__} — {e}")
Raising exceptions yourself¶
def set_age(value):
if value < 0:
raise ValueError("age cannot be negative")
if value > 150:
raise ValueError("age too large to be realistic")
return value
try:
set_age(-5)
except ValueError as e:
print("Caught:", e)
Custom exception classes¶
For complex apps, define your own — gives callers a meaningful type to catch.
class InsufficientFundsError(Exception):
"""Raised when a withdrawal exceeds balance."""
class Account:
def __init__(self, owner, balance):
self.owner, self.balance = owner, balance
def withdraw(self, amount):
if amount > self.balance:
raise InsufficientFundsError(
f"{self.owner} has ₹{self.balance}; can't withdraw ₹{amount}"
)
self.balance -= amount
# Use it
acc = Account("Alice", 1000)
try:
acc.withdraw(5000)
except InsufficientFundsError as e:
print("⚠️", e)
try with file I/O¶
try:
with open("does_not_exist.txt") as f:
content = f.read()
except FileNotFoundError:
print("File not found — using default content")
content = "DEFAULT"
print(content)
try with multiple operations¶
def parse_score(text):
try:
score = int(text)
if score < 0 or score > 100:
raise ValueError(f"score {score} out of range 0..100")
return score
except ValueError as e:
print(f"Bad score '{text}': {e}")
return None
inputs = ["85", "120", "abc", "50"]
for s in inputs:
print(f"{s!r:>6} → {parse_score(s)}")
Best practices¶
- Catch specific exceptions —
except ValueError:notexcept:. A bareexceptcatches everything includingKeyboardInterrupt, masking real bugs. - Don't swallow errors silently — at least log them.
finallyfor cleanup — butwithis usually cleaner.- Fail fast — raise as soon as you detect a problem. Don't continue with bad data.
- Don't use exceptions for control flow —
if/elseis clearer for normal logic.
Bad — too broad:
try:
do_something()
except: # catches EVERYTHING, including bugs
pass # silent failure — debugging nightmare
Good — specific + informative:
try:
do_something()
except ValueError as e:
logger.warning("bad value: %s", e)
raise # re-raise so caller knows
Mini-project — robust file reader¶
def read_config(path):
try:
with open(path) as f:
import json
return json.load(f)
except FileNotFoundError:
print(f"Config '{path}' not found — using defaults.")
return {"theme": "light", "lang": "en"}
except json.JSONDecodeError as e:
print(f"Config '{path}' is corrupt: {e}")
return None
except PermissionError:
print(f"No permission to read '{path}'.")
return None
# Create a corrupt config
with open("bad-config.json", "w") as f:
f.write("{not valid JSON}")
print(read_config("missing.json"))
print(read_config("bad-config.json"))
Practice¶
What does this print?
Expected: caughtcleanup
Catch the specific error, not a bare except
Expected: division by zero
Quiz — Quick check¶
What you remember
Q1. Which exception is raised by int("hello")?
-
TypeError -
ValueError -
NameError -
SyntaxError
Why: The argument is the right type (str) but the wrong value —
int()can't parse "hello".TypeErroris for wrong-type situations like"a" + 5.
Q2. When does the finally block run?
- Only if the try block succeeded
- Only if an exception was caught
- Always — whether the try succeeded, raised, or even returned early
- Never — it's deprecated
Why:
finallyis for cleanup code that must run no matter what. Closing files, releasing locks, restoring state.
Q3. What's wrong with except: (a bare except)?
- It's slow
- It catches every exception, including system signals like Ctrl+C, hiding real bugs
- It's a syntax error
- Nothing — it's recommended
Why: Bare
except:catchesKeyboardInterrupt,SystemExit, and any unforeseen error — making it nearly impossible to debug or even kill the program cleanly. Always catch specific exceptions.
Common doubts¶
When should I raise instead of return None?
Use raise when something is unexpectedly wrong — invalid input, missing file, broken contract. Use return None (or a sentinel) when "nothing found" is a normal outcome — dict.get(key), parsing optional fields. Exceptions are for failures; return values are for results.
Should I create custom exception classes?
Yes, for non-trivial apps. Custom exceptions let callers catch your errors specifically: except InsufficientFundsError: is clearer than except ValueError: and won't accidentally catch unrelated value errors. Subclass Exception (not BaseException).
Is it bad to catch exceptions just to ignore them?
Almost always yes. "Swallowing" exceptions hides bugs. At minimum, log them. The rare valid case: a non-critical operation (e.g. updating a cache) where failure should not stop the main flow — but even then, log the exception with logging.exception() so it's not invisible.