Things go wrong in every real program — a user types text where a number was expected, a file doesn't exist, a network call times out. Python's try/except/else/finally block is how you catch those problems and keep the program running instead of crashing outright.
Catching Specific Exceptions
def safe_divide(a, b):
try:
result = a / b
except ZeroDivisionError:
print("Error: cannot divide by zero")
return None
except TypeError:
print("Error: both arguments must be numbers")
return None
else:
print("Division succeeded")
return result
finally:
print("Division attempt finished")
print(safe_divide(10, 2))
print("---")
print(safe_divide(10, 0))
Output:
Division succeeded
Division attempt finished
5.0
---
Error: cannot divide by zero
Division attempt finished
None
Each part of the block has a distinct job:
try— the code that might fail goes here.except ZeroDivisionError/except TypeError— catches specific, named exception types. Catching specific exceptions (rather than a bareexcept:) matters because a bare except silently swallows every error, including genuine bugs you'd actually want to see.else— runs only if thetryblock completed with no exception at all.finally— always runs, whether an exception happened or not — the right place for cleanup code like closing a file or a network connection.
Raising Your Own Exception
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
print(f"Age set to {age}")
try:
set_age(-5)
except ValueError as e:
print(f"Caught an error: {e}")
Output:
Caught an error: Age cannot be negative
The as e part captures the exception object itself, so you can inspect its message (or any other data it carries) instead of just knowing that *something* went wrong. This same pattern — validate, then raise ValueError(...) — is how most of Python's own built-in functions signal bad input, which is why catching ValueError and TypeError specifically is so common in real code.
