A try block can do more than catch errors. Python adds two optional clauses, else and finally, that solve problems except alone cannot. else holds code that should run only when nothing went wrong. finally holds cleanup that has to happen regardless of what went wrong. This lesson covers when each one runs, why they exist, how they interact with return, the trap of returning from finally, and how the whole pattern compares to a with statement.
A try statement can have up to four sections, each running at a specific moment.
| Clause | When it runs | Typical job |
|---|---|---|
try | First, always | The risky operation |
except | Only if a matching exception is raised in try | Recovering from a known failure |
else | Only if try finished with no exception | Code that depends on the try succeeding |
finally | Always, no matter what happened | Cleanup that must run on every path |
This lesson focuses on the bottom two rows: else and finally.
The diagram captures the rule that drives the rest of this lesson. finally is always on the path. else is only on the no-exception path. The handler is only on the matched-exception path.
else RunsThe else clause runs after the try block, but only when the try finished cleanly without raising. If an exception was raised, the else is skipped, whether or not the exception got caught. If a finally is present, it still runs after else.
The first call succeeds. The try looks up the price, no exception, control goes to else, and the formatted line prints. The second call misses on the lookup, raises KeyError, the handler runs, and the else is skipped entirely.
Two details. The variable price from inside the try is visible in else. Both bodies live in the same enclosing scope, so the binding survives. And the else only runs once, after the whole try body completes. It isn't a loop and it doesn't repeat per statement.
A second example shows how else keeps different concerns apart even when several operations might fail:
The try only protects the part that could legitimately fail at that line: reading the "items" key. The actual summation lives in else. If sum or the inner lookup ever raised a different exception (a KeyError for a missing product, say), it would propagate from else rather than from try, so the existing handler wouldn't accidentally catch it.
else doesn't introduce any extra overhead. It's a normal block of code that Python runs on one particular path.
else: Keep the try Body MinimalThe most common reason to use else is to keep the try block as small as possible. A try body should ideally contain only the lines that can actually raise the exception being caught. Anything else belongs outside the try, and else provides a clean place to put it without leaving the structure.
Compare these two versions of the same function:
The try block is too wide. The intent is to catch a KeyError from a missing price. But save_to_database and send_confirmation_email can also raise their own exceptions, and if any of them happens to raise KeyError for unrelated reasons (a missing customer field, a missing config key, anything), the handler will catch it and report the wrong cause. The bug runs without a trace: the function will print "missing price for ..." when the real failure was in the email step.
Splitting it with else fixes the problem:
Now the try block does one thing, and the handler can only catch failures from that one thing. The follow-up work (saving, emailing, logging) lives in else, where a KeyError from any of those would propagate as a real error rather than being mistaken for a price problem.
There's a second benefit. Reading the structure shows exactly what the function expects to fail and what it does on success. The try says "this is what might break." The except says "this is what we do about it." The else says "this is what we do when nothing broke." Three sections, three meanings, no overlap.
finally RunsThe finally clause runs no matter what happened in the rest of the block. Exception raised, exception caught, exception uncaught, normal completion, early return, break, or continue inside a loop, none of these skip finally. The clause runs before control leaves the try structure, every time.
A small demonstration that prints what happened on each path:
Three scenarios show the three main shapes. The clean run hits try, else, finally, in that order. The caught run hits try, except, finally; else is skipped because the try raised. The uncaught run hits try, finally, and then the exception propagates to the outer handler. (The third scenario's output is missing the --- uncaught --- header in some Python builds because the output is interleaved with the traceback. finally always runs.)
The most useful place for finally is cleanup that must happen regardless of outcome: closing a file, releasing a lock, undoing a temporary state change, decrementing a counter. The block is "the thing that has to happen on the way out, no matter how the function is leaving."
finally: Releasing ResourcesResources that need explicit cleanup show up often. Open files, database connections, network sockets, locks, and temporary directories all need to be released when the work is done. If the code raises an exception in the middle of using one, the resource still needs to be released. finally is the mechanism that guarantees it.
The classic example is closing a file:
If split raises a ValueError for a malformed line, the function bails out with that exception, but finally runs first and closes the file. If float raises for a non-numeric price, same thing. If the loop completes and the function returns the dict, finally still runs on the way out. The file is released on every path.
A lock follows the same shape:
Both calls release the lock. The successful one releases it on the way out of the try. The failing one raises ValueError, finally runs and releases the lock, and then the exception propagates to the caller. Without finally, the second path would leak the lock and every thread waiting on it would block forever.
A transaction that has to commit or roll back fits the same pattern:
The successful transfer commits inside the try, and finally sees tx.committed = True and does nothing extra. The failing transfer raises before reaching commit, so finally rolls back and then lets the exception propagate. Either way the transaction reaches a terminal state, which is exactly what database-style code needs.
finally and return: A Subtle Rulefinally runs even when the try block contains a return statement. That seems obvious until tracing through a function that returns from both try and finally. The rule Python follows is: evaluate the return value first, then run finally, and if finally doesn't itself return a different value, hand back the originally evaluated value.
Python evaluates inventory[product] and stashes 5 as the return value. Before the function returns to the caller, finally runs and prints its line. Then the stashed 5 is delivered. The order shows up clearly: finally ran prints first, then the 5 from print(get_stock(...)).
The same rule applies when the try block raises and there is no matching except. Python remembers the exception, runs finally, and then propagates the exception:
The int call raises, finally runs its print, and the ValueError is still on its way up to the outer handler. The cleanup happens, then the exception finishes propagating. This is the property that makes finally work for resource cleanup: even when the function is being unwound because of an error, the cleanup code still runs first.
return Inside finallyfinally always runs, and finally is allowed to contain its own return. Put those two facts together and the result is a sharp edge. If finally returns a value, it replaces whatever the try/except/else was going to return. It also suppresses any exception that was propagating out. Real errors disappear; real return values get overridden.
The first call should return 5. The second should raise KeyError because of the explicit raise. Both calls return -1. The return -1 inside finally overrides whatever the try or except was going to do. Python doesn't warn about this. The function looks like it has a real implementation, and the bug only shows up when every call comes back with the same -1.
The same effect happens if finally raises a new exception:
The try succeeded and was about to return 5. The finally raises a fresh RuntimeError, which replaces the return value entirely. If the try had been raising its own exception, the new exception would replace that one too. Python at least chains them so the original shows up in the traceback as During handling of the above exception, another exception occurred, but the surface exception is the new one.
Two rules:
try, do it in else or after the whole structure.finally as a place where things should not themselves fail.A safer version of the same function uses a flag:
The finally block does its logging work without returning anything, so the original return values flow through unchanged.
finally with break and continue in LoopsA try/finally inside a loop interacts with break and continue the same way it interacts with return. The finally runs first, then the loop control statement takes effect. This is useful for cleanup that has to happen even when a loop is bailing out early.
The first iteration's try doesn't return, so the loop continues. The second iteration's try does return, but finally runs first and prints its line. The loop never reaches the third iteration. finally still runs on the iteration that returned. That guarantee is what makes the clause reliable for cleanup.
with)Many of the resource-cleanup patterns above are common enough that Python has a special syntax for them: the with statement. A with block uses a context manager, an object that knows how to set up a resource on entry and tear it down on exit automatically. Internally, with is built on top of try/finally with some extra machinery; from the outside it reads as cleaner.
The earlier file-handling example with try/finally:
Rewritten with with:
The two versions behave identically. The file gets closed on success, on exception, on early return, in every case. The with form is shorter, harder to forget, and reads as "use this file inside this block." For files, locks, database connections, and anything else that needs deterministic cleanup, with is the standard Python idiom.
That doesn't make try/finally obsolete. There are still situations where the direct form fits:
try/except for error handling is mixed with cleanup that has nothing to do with the exception. finally combines the two without nesting another with inside the try.A side-by-side comparison shows when to use each:
| Situation | Best tool | Why |
|---|---|---|
| Closing a file, database connection, or lock | with | Standard idiom, automatic, harder to forget |
| Generic cleanup that isn't a resource | try/finally | Context managers are for resource lifecycles |
You also need except to handle a specific error | Either, mixed | with for the resource, try/except around it |
Cleanup that depends on whether the try succeeded | try/else | Run code only on the no-exception path |
The typical layered shape: with for resources, try/except for known failure modes, else for the happy-path follow-up, and finally only for non-resource cleanup that doesn't fit anywhere else. The four building blocks compose cleanly.
The takeaway: when try/finally is being used purely to close a file or release a lock, the more Pythonic form is with.
A realistic end-to-end example uses every clause in this section. The function loads a list of order lines from a CSV, parses each one, totals the order, prints a summary, and tracks how many attempts it made regardless of outcome.
Reading top to bottom: the try opens the file with with, so the file is closed automatically no matter how this part exits. Inside the loop, each line is parsed and added to the total. If the file doesn't exist, FileNotFoundError is caught and 0.0 is returned. If the file exists but contains malformed data, the ValueError (from split or int) or KeyError (from the catalog lookup) is caught, logged with the full traceback, and 0.0 is returned. If everything works, else logs the success and returns the real total. finally always logs the attempt counter, regardless of which path was taken.
Three structural points. The with handles file cleanup, so no separate try/finally for that is needed; the higher-level idiom takes over. else runs only on the success path, so the success log message can't be confused with a failure case. finally runs on every path (success, missing file, malformed data), so the counter stays accurate even when the function bails out early.
That's the shape exception handling tends to take in Python code: small, focused try blocks; specific except types; an else for the happy-path follow-up; a finally for cleanup or instrumentation that has to happen no matter what; and with for the resources that have their own context managers.
10 quizzes