Python

Exceptions, and the with statement that always cleans up

try/except/else/finally in the order they actually run, and what a context manager guarantees.

After this lesson you can

  • Say what each of try, except, else and finally is for
  • Catch a specific exception type rather than a bare except
  • Explain what a with statement guarantees that a plain try does not

try has four parts, and each one has a distinct job:

try:
    value = risky()
except ValueError as e:
    print(f"bad value: {e}")
except (TypeError, KeyError) as e:
    print(f"bad shape: {e}")
else:
    print("no exception, value is ready")
finally:
    print("always runs, exception or not")

except only catches what actually raised — code in else is not guarded, which is exactly the point: else is where you put code that should run only when nothing went wrong, and a bug inside it is not quietly swallowed by the except above it. finally runs no matter what: on success, on a caught exception, on an exception that was not caught, even after a return inside try.

A bare except: catches everything, including KeyboardInterrupt and SystemExit — the two exceptions a program almost always wants to actually stop for. Catch the specific type you expect and know how to handle; let everything else propagate.

Try it

else runs only when nothing was raisedpython-3.12
def attempt(value):    steps = []    try:        if value < 0:            raise ValueError("negative")        steps.append("try")    except ValueError:        steps.append("except")    else:        steps.append("else")    finally:        steps.append("finally")    return steps  def run():    return {"ok": attempt(5), "bad": attempt(-1)}
What to look for

Raising your own exceptions

class InsufficientFundsError(Exception):
    def __init__(self, needed, available):
        super().__init__(f"needed {needed}, had {available}")
        self.needed = needed
        self.available = available

def withdraw(balance, amount):
    if amount > balance:
        raise InsufficientFundsError(amount, balance)
    return balance - amount

A custom exception class is a real class, usually with almost no body — its job is to give the except block something specific to catch, and to carry whatever data the caller will need to react.

EAFP over LBYL

Python's idiom is "easier to ask forgiveness than permission": try the operation, catch the failure, rather than checking every precondition first.

# LBYL — look before you leap
if key in d and isinstance(d[key], int):
    value = d[key]

# EAFP — the Python way
try:
    value = d[key]
except KeyError:
    value = None

EAFP wins for two reasons: the check-then-use version can still race (something removes key between the check and the use, in a threaded or async program), and it usually reads as one clear path instead of a guard clause in front of it.

with — cleanup that cannot be skipped

with open("data.txt") as f:
    contents = f.read()
# f.close() has already run here, even if read() raised

A with block calls the context manager's cleanup (__exit__) whether the block finished normally, returned early, or raised — the same guarantee finally gives a try, but attached to the object that owns the resource instead of hand-written at every call site. open, database connections, and locks all implement this, which is why with is the default reach for any of them.

Try it yourself

2 visible tests · 2 hidden tests

Implement entryPoint(a, b). Return {"ok": True, "value": a / b} when the division succeeds. When b is 0, catch the ZeroDivisionError and return {"ok": False, "value": None} instead of letting it propagate.

  • entryPoint(10, 2)
  • entryPoint(7, 0)
Loading editor…

Sign up to check the hidden tests and save your progress. Sign up