When you work with external resources such as a database or temporary files, you often need to run some cleanup actions after you've done the work.
Python provides two options - the context manager and the try/finally block. Both are valid options, but the context manager is often lauded as being more Pythonic.
Despite this, try/finally block is still widely used. As we will discover, try/finally is simple to use and well-suited in some cases, but it does come with pitfalls, as the messenger tragically discovered.
Let us try and contact the messenger.
A variant of the try/finally pattern exists in most languages1 and functions pretty much in the way you might imagine. Consider this trivial example:
The interpreter enters the first block and executes the statement. The interpreter then proceeds to the finally block. The final block always executes. It does not matter if the first block succeeds or fails.
If a failure is raised during execution as shown below, the error disrupts the normal flow and the interpreter stops executing the try block immediately. The interpreter then jumps directly to the finally block.
If you choose to handle the failure using an except block as shown below, the error is caught and handled by the except block before proceeding to the finally block. An error could also occur inside the except or else block. The interpreter would still execute the finally block before raising the new error.
This brings us to the behavior of returning values. A function can return a value from within the try block. When the interpreter encounters this return statement, It prepares to send this value back to the caller. But first, it must honor the finally block. So it pauses the return process and executes the finally block first before returning the prepared value from the try block.
The finally block can also contain its own return statement, as shown below. When this happens, the return in the finally block wins and the return value from the try is effectively ignored.
This behavior applies to exceptions as well. We can place a loop around our structure to observe break and continue statements.
A break statement inside a finally block will swallow any unhandled exception from the try block. A continue statement will do the exact same thing. The exception disappears completely.
Lets examine a more complex example with nested try/finally statements. Do return statements break out of parent try/finally blocks?
No, try/finally statements can be nested, and return statements from child blocks do not prevent parent finally blocks from running. However, the value from the last return statement trumps the rest and is still the one that is returned by the function.
As you can imagine, any language which allows you to write dead code that produces unintended outcomes is problematic. The Python language developers recognized this danger, and proposed that return/break/continue statements should be disallowed in finally blocks in PEP6012.
However, it was voted down for the following reason:
Reading the references in the PEP it seems to me that most languages implement this kind of construct but have style guides and/or linters that reject it. I would support a proposal to add this to PEP 8 (if it isn’t already there).
I note that the toy examples are somewhat misleading – the functionality that may be useful is a conditional return (or break etc.) inside a finally block.
-Guido 2019 PEP601
Guido’s reasoning was that there may be valid scenarios where the user requires full control of exception handling in the finally block, and may wish to override the raising of exceptions. Preventing this behavior would effectively hamstring advanced users.
However, in 2024 the community tried again with PEP7653. This time they were armed with evidence. They analyzed the top 8000 PyPi packages and found that:
Most of the usages (of
returninfinally) are incorrect, and introduce unintended exception-swallowing bugs. - PEP765
This was enough for the proposal to get over the line, and from Python 3.14 onwards, using return, break or continue in a finally clause emits a SyntaxWarning.
A context manager is the pythonic choice most of the time when you’re working with resources that already expose acquire/release semantics that can easily slot into __enter__ and __exit__. It’s the natural choice for files, locks, database connections, temporary state changes, etc. It’s declarative and minimizes surface area for errors.
However, they don’t always make sense:
Context manager code lives in a different location, and so introduces an extra layer of abstraction. Sometimes your code is so simple and localized that introducing an extra layer of abstraction would make the code less readable with little benefit.
Context managers can only use variables passed in during initialization, whereas finally can reference variables mutated during execution in the try block.
With try/finally, you can combine
exceptandfinallyclauses to explicitly manage different failure modes for more granular control.
The novice believed the original message was secure. The master understood the final seal controls the truth.
The finally block always speaks last. You must ensure its final words do not obscure the truth of what came before.
