Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 leave

except 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
A: 1
B: bad seed
C: bad seed

B 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ORDER = [41, 42, 43]
print(ORDER.index(41))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
0

list.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:

Azvya Education Pvt. Ltd.VLSI Mentor
withelse.py (BROKEN ON PURPOSE)
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))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 list

A 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.

try is for the risky line. else is 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
try:
    run()
except ToolError:
    record()
finally:
    cleanup()     # always

5. finally Runs Even When the Exception Escapes

This is the property that makes it a guarantee rather than a convenience:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def ordered():
    try:
        print("  body")
        return "returned"
    finally:
        print("  finally")


print(ordered())
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  body
  finally
returned

The 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
def swallow():
    try:
        raise ValueError("the parser found nothing")
    finally:
        return "looks fine"


print(swallow())
print("the program continued")
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
looks fine
the program continued

A 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:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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)
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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]))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
  measured 3 values
A: 2.0

The 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

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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))
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
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 try has four clauses in the order try, except, else, finally.
  • else runs only when nothing was raised, and is not guarded by the handlers above it.
  • Keep the try block to the risky line; put the follow-on work in else so a different failure cannot be misreported.
  • except and else are mutually exclusive.
  • finally always runs — on success, on a handled failure, and while an unhandled exception is leaving.
  • finally runs before a return hands its value back, so return does not mean "leave immediately".
  • Each frame's finally runs as the stack unwinds, innermost first.
  • Never return from a finally — it replaces the in-flight exception and destroys it without trace.
  • try/finally with no except is a normal and useful shape.
  • For files, Module 15's with replaces this — it is try/finally with 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.