Skip to main content

try / except / finally & custom exceptions

Level 2: Level 2: Idiomsmedium12 minexceptionstry-exceptraisecustom-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
Check yourself
safe_divide wraps a / b in try and catches ZeroDivisionError, returning None. A caller passes b as the string '2' instead of a number. What does that caller see?

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:

Table
else is the block people forget, and it exists to keep the try body narrow: code that must not be guarded goes in else, so an exception raised there is not caught by your own except.
BlockRuns whenTypical use
tryalways, firstthe operation that might fail
exceptonly if a matching exception was raisedhandle that specific failure
elseonly if NO exception was raisedthe follow-up work that must not be inside try
finallyalways, last, even on return or re-raisecleanup: close the file, release the lock
else is the block people forget, and it exists to keep the try body narrow: code that must not be guarded goes in else, so an exception raised there is not caught by your own except.
try:
    risky()
finally:
    cleanup()             # runs on success and on failure
Check yourself
A function's try block does return 'a' and its finally block does return 'b'. What does calling that function give you?

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
Check yourself
You guard a division with a bare except: that returns None. Months later someone mistypes a variable name on a line inside that same try block. What does the caller see?

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.

Worked example (Python)
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))    # None

Apply

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.