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
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)}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 testsImplement 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)
Sign up to check the hidden tests and save your progress. Sign up