try / except / finally & custom exceptions
Handle errors cleanly, raise your own, and define a custom exception.
Why catching errors matters
In real services the input is never clean: a user posts b=0, a config file is missing, an upstream API returns text where you expected a number. When one of those operations raises and nothing catches it, the exception unwinds up the call stack and the whole request (or batch job) dies. Exception handling is how you draw a boundary around risky code: this line might fail, and here is exactly what I do when it does. A pipeline that skips one malformed row is useful. One that crashes on row 3 of 10 million rows is not.
The mental model
When a statement raises, Python stops the current block immediately and unwinds outward looking for a matching handler. A try/except installs that handler for a specific region:
try:
result = a / b # if this raises...
except ZeroDivisionError:
result = None # ...jump here, but only for this error type
except ZeroDivisionError matches that class and its subclasses. Any other error (a TypeError, say) is not caught here and keeps propagating. Catch the specific exception you expect, not everything. That is exactly what safe_divide in the demo below does: it returns 5.0 for safe_divide(10, 2) and None for safe_divide(5, 0), and it never hides an unrelated bug.
finally always runs
A finally block runs whether the try succeeded, raised, or returned early. Use it for cleanup that must happen no matter what, like closing a file or releasing a lock:
| Block | Runs when | Typical use |
|---|---|---|
| try | always, first | the operation that might fail |
| except | only if a matching exception was raised | handle that specific failure |
| else | only if NO exception was raised | the follow-up work that must not be inside try |
| finally | always, last, even on return or re-raise | cleanup: close the file, release the lock |
try:
risky()
finally:
cleanup() # runs on success and on failure
Raising your own
Use raise to signal a failure yourself, and subclass Exception to give that failure a name so callers can catch exactly your error and nothing else:
class TooSmallError(Exception):
pass
def validate(n):
try:
if n < 10:
raise TooSmallError()
return "ok"
except TooSmallError:
return "too small"
print(validate(5)) # too small
print(validate(10)) # ok
Pitfalls
A bare except: (or except Exception:) catches too much. If you wrap a / b in except: and later mistype a variable name, the resulting NameError gets swallowed and you silently return None, hiding a real bug. Catch the narrow type instead. A second trap: a return inside finally overrides any return value or exception from the try and discards it silently, so do not return from finally.
Interview nuance: Python idiom favors EAFP ("easier to ask forgiveness than permission") over LBYL ("look before you leap"). Rather than checking if b != 0: before dividing, you try the division and catch ZeroDivisionError. This is not just style. The check-first approach has a race window in concurrent code (a shared value can change between the check and the use), and in CPython entering a try block costs nothing on the non-error path, so exception handling is effectively free when no error is raised. Interviewers watch for whether you catch a specific exception type or reach for a bare except.
def safe_divide(a, b):
try:
return a / b
except ZeroDivisionError:
return None
print(safe_divide(10, 2)) # 5.0
print(safe_divide(5, 0)) # NoneApply
Your turn
The task this lesson builds to.
Implement safe_divide(a, b): return a / b, but if b is 0 (a ZeroDivisionError),
return None instead of crashing.
3 hints and 4 automated checks are waiting in the workspace.
Practice
Make it stick
A second problem on the same idea, so it survives past today.
Define a custom TooSmallError(Exception). In validate(n), raise it when n < 10,
catch it, and return "too small"; otherwise return "ok".
validate(5) returns "too small"; validate(10) returns "ok".
2 hints and 4 automated checks are waiting in the workspace.