Python Part I · Module 14 · Errors and Exception Handling
else and finally: Cleanup That Always Runs
A try statement has four clauses, and most people use two. The other two exist because of a problem that is easy to create and hard to see: once you wrap a region of code in a handler, everything in that region is guarded, including the lines that were never risky — so a handler written for one failure can quietly catch a different one and report the wrong cause. One of these clauses fixes that. The other guarantees that whatever you acquired gets released even when the function is on its way out with an exception, and it carries the single most dangerous construct in this module.
Intermediate16 min readPythonPart IFundamentalsErrorsExceptions
Part I · Module 14 · Chapter 14.6 · else and finally: Cleanup That Always Runs
1. Four Clauses
try:
... # the guarded code
except SomeError:
... # ran only if SomeError was raised
else:
... # ran only if NOTHING was raised
finally:
... # ran always, no matter how we leaveexcept you have. This chapter is the other two, and the order above is the
order Python requires.
2. The Problem else Solves
Here is a handler that lies. ORDER is a list of seeds in priority order, and
the function reports where a given seed sits in it:
ORDER = [41, 42, 43]
def rank_of(raw, order):
try:
seed = int(raw)
return order.index(seed)
except ValueError:
return "bad seed"
print("A:", rank_of("42", ORDER))
print("B:", rank_of("n/a", ORDER))
print("C:", rank_of("99", ORDER))A: 1
B: bad seed
C: bad seedB and C report the same thing, and only B is true. "n/a" really is a bad
seed. "99" is a perfectly valid seed — it simply is not in this particular
priority list, which is an entirely different situation and probably not an
error at all.
The handler cannot tell them apart, and the reason is not carelessness about
types. Both lines in that try block raise ValueError:
ORDER = [41, 42, 43]
print(ORDER.index(41))0list.index raises ValueError when the item is absent — the same type int()
raises for unparseable text. So except ValueError catches both, and the
message written for one of them gets attached to both.
This is the trap. It is not that the wrong type was caught; it is that the
try block contained more than the risky line, and the extra line happened to
fail the same way.
3. else Runs Only on Success
else holds the code that should run when the try succeeded, and it is not
guarded by the handlers:
ORDER = [41, 42, 43]
def rank_of(raw, order):
try:
seed = int(raw)
except ValueError:
return "bad seed"
else:
return order.index(seed)
print("A:", rank_of("42", ORDER))
print("B:", rank_of("n/a", ORDER))
print("C:", rank_of("99", ORDER))A: 1
B: bad seed
Traceback (most recent call last):
File "withelse.py", line 15, in <module>
print("C:", rank_of("99", ORDER))
~~~~~~~^^^^^^^^^^^^^
File "withelse.py", line 10, in rank_of
return order.index(seed)
~~~~~~~~~~~^^^^^^
ValueError: 99 is not in listA and B are unchanged. C now tells the truth: ValueError: 99 is not in list,
on the line that actually failed. The lie is gone, and what replaced it is a
precise description of a case the author had not thought about — which is
exactly what you want a crash to give you.
tryis for the risky line.elseis for what you do afterwards.
The discipline is mechanical: put one operation in the try block whenever
you can, and everything that follows it in else. The narrower the block, the
less the handler can misattribute.
4. finally Runs No Matter What
finally is the guarantee. It runs on success, on a handled failure, and on an
unhandled one:
def trial(x):
log = []
try:
log.append("try")
_ = 10 / x
except ZeroDivisionError:
log.append("except")
else:
log.append("else")
finally:
log.append("finally")
return log
print("success:", trial(2))
print("failure:", trial(0))success: ['try', 'else', 'finally']
failure: ['try', 'except', 'finally']Two things to read off: else and except are mutually exclusive, and
finally is in both lists.
The canonical shape for this chapter is exactly that:
try:
run()
except ToolError:
record()
finally:
cleanup() # always5. finally Runs Even When the Exception Escapes
This is the property that makes it a guarantee rather than a convenience:
def acquire_and_fail(registry):
registry.append("scratch-dir")
try:
return 10 / 0
finally:
registry.remove("scratch-dir")
print(" cleanup ran")
held = []
try:
acquire_and_fail(held)
except ZeroDivisionError:
print("caught outside")
print("still held:", held) cleanup ran
caught outside
still held: []There is no except clause in that function at all. The exception was on
its way out, and finally still ran before it left. That is why cleanup belongs
there and nowhere else: a plain line after the try statement would have been
skipped entirely.
Note the order of the output. The cleanup happened before the outer handler
saw the exception — the stack unwinds one frame at a time, running each
finally as it goes.
It also runs before a successful return hands its value back:
def ordered():
try:
print(" body")
return "returned"
finally:
print(" finally")
print(ordered()) body
finally
returnedThe return expression is evaluated, then finally runs, then the value is
handed over. return does not mean "leave immediately".
6. The Trap: return Inside finally
Now the dangerous one. Because finally runs while an exception is in flight,
a return there replaces whatever was happening — including the exception:
def swallow():
try:
raise ValueError("the parser found nothing")
finally:
return "looks fine"
print(swallow())
print("the program continued")looks fine
the program continuedA ValueError was raised, carried a real message, and vanished. No
traceback, no handler, no record anywhere that anything went wrong. The function
returned a normal-looking string and the program carried on.
7. A Realistic Shape
Putting the four clauses together, with a registry standing in for an acquired resource:
HELD = []
def run_stage(name, fail):
HELD.append(name)
try:
if fail:
raise RuntimeError(f"{name} did not finish")
result = f"{name}: ok"
except RuntimeError as e:
result = f"{name}: FAILED ({e})"
else:
result = result + " (clean)"
finally:
HELD.remove(name)
return result
print(run_stage("elaborate", False))
print(run_stage("simulate", True))
print("leaked resources:", HELD)elaborate: ok (clean)
simulate: FAILED (simulate did not finish)
leaked resources: []Each clause does one job: the try raises, except records, else adds the
part that only makes sense on success, and finally releases the resource in
both paths. leaked resources: [] is the assertion that the guarantee held.
8. finally Without except
A try/finally with no handler at all is a completely normal thing to write,
and Section 5 is the proof. It says: I am not handling anything here, but this
cleanup must happen on the way out whatever happens.
def measure(values):
depth = len(values)
try:
return sum(values) / depth
finally:
print(f" measured {depth} values")
print("A:", measure([1.0, 2.0, 3.0])) measured 3 values
A: 2.0The exception, if values were empty, would propagate untouched — and the note
would still print.
9. Files Are the Real Use Case, and They Have a Better Answer
Everything above models a resource with a list, because the resource you will actually need to release is a file, and files belong to Module 15.
Module 15 Chapter 15.5 introduces with, which does this job in one line and
cannot be forgotten. When you get there, recognise it as the pattern in this
chapter with the bookkeeping removed — with is try/finally with the
finally written for you.
Hand-rolled try/finally remains correct for anything without a with
form: a scratch directory you created, a lock you took, a configuration you
changed and must change back.
10. Predict, Run, Observe
def mystery(x):
order = []
try:
order.append("try")
if x == 0:
raise ZeroDivisionError("boom")
order.append("try-end")
except ZeroDivisionError:
order.append("except")
else:
order.append("else")
finally:
order.append("finally")
order.append("after")
return order
print("A:", mystery(1))
print("B:", mystery(0))A: ['try', 'try-end', 'else', 'finally', 'after']
B: ['try', 'except', 'finally', 'after']A — the whole try body ran, then else, then finally, then the code
after the statement.
B — try-end is missing, exactly as Chapter 14.4 Section 3 showed, and
else is absent because else means nothing was raised.
finally and after are in both. Once an exception is handled, execution
continues normally, so the line after the whole statement runs either way.
else sits between except and finally in the source but runs before
finally — reading order and execution order agree here, which is not true of
except.
11. Exercises
Exercise 1 — the four clauses. Write one try statement using all four and
print a marker from each. Run it twice, succeeding and failing.
Exercise 2 — the lying handler. Reproduce Section 2's guarded function and
construct an input where the handler reports the wrong cause. Then fix it with
else and show the traceback now points at the real line.
Exercise 3 — the guarantee. Write a function that appends to a list, raises,
and removes in finally. Call it inside a handler and assert the list is empty
afterwards.
Exercise 4 — order of unwinding. Nest two functions that each have a
finally, and raise in the inner one. Record the order the two cleanups run in
and explain it.
Exercise 5 — return ordering. Write a function with a return in try and a
print in finally. Predict which appears first, then run it.
Exercise 6 — the swallow. Reproduce Section 6 and confirm the exception disappears. Then write one sentence you would say in code review.
Exercise 7 — no except. Write a try/finally with no handler where the
exception propagates and the cleanup still runs. Explain why this is not the
same as catching it.
Exercise 8 — else or not. For each, say whether the line belongs in try or
else: (a) converting a field (b) storing the converted value in a dict (c)
looking up a key that must exist (d) printing a success message.
12. Summary
- A
tryhas four clauses in the ordertry,except,else,finally. elseruns only when nothing was raised, and is not guarded by the handlers above it.- Keep the
tryblock to the risky line; put the follow-on work inelseso a different failure cannot be misreported. exceptandelseare mutually exclusive.finallyalways runs — on success, on a handled failure, and while an unhandled exception is leaving.finallyruns before areturnhands its value back, soreturndoes not mean "leave immediately".- Each frame's
finallyruns as the stack unwinds, innermost first. - Never
returnfrom afinally— it replaces the in-flight exception and destroys it without trace. try/finallywith noexceptis a normal and useful shape.- For files, Module 15's
withreplaces this — it istry/finallywith the cleanup written for you.
So far every exception has been raised by Python. The next chapter raises one deliberately, which is how a script says this input makes no sense at the moment it can still be acted on.
Where this fits
Part of the Python curriculum.
