Context managers and the with statement
The topic combines context managers with the with statement, while the supplied mechanism is explained through the finally guarantee.
Try it: Context Managers and the with Statement
What with really does: it calls __enter__ before the block and __exit__ after it — even when an exception leaves the block — so a file opened with with is always closed before the exception continues; try/finally gives the same guarantee by hand, a bare close() is skipped by the exception, and a context manager whose __exit__ returns True suppresses the exception.
How it works
- with EXPR as f: evaluates EXPR (e.g. open(...)), calls its __enter__() and binds the returned value to f (a file's __enter__ returns the file itself).
- The block runs. If a statement raises (here int() on text that is not a number), the rest of the block is skipped.
- Whether the block finished or an exception is leaving it, Python calls __exit__(exc_type, exc, tb) — for a file this closes it. This happens before any outer except sees the exception.
- If __exit__ returns a true value the exception is suppressed and execution continues after the block; a file's __exit__ returns None, so the exception propagates to the nearest matching except (or stops the program).
- try: … finally: f.close() gives the same cleanup by hand (start with f = None and check it); open() … f.close() without either leaves the file open when an exception jumps past close().
Default run (10 steps): The program opens data.txt, which contains '4x\n'. Watch when the file is closed — and what happens if int() raises ValueError. … The program finished. The file was closed.
Simplified: Fixed program templates reading one line of data.txt (text you type, up to 12 ASCII characters) and converting it with int(). A small interpreter written for these statements runs them — nothing is executed — and it reproduces CPython 3.12's sys.settrace line/call/exception events, printed output, final error line and file state for every combination. File objects' own __enter__/__exit__ are C code, so they appear as protocol steps rather than traced lines.
Loading the simulation…
Why Cleanup Cannot Wait
Opening a file asks the operating system to allocate a resource called a file handle. The file begins in an open state. If an exception occurs before the file is closed, the handle can remain open. This resource leak wastes system memory and can prevent other programs from accessing the file.
The difficulty is that an exception can interrupt the normal path at many points. Cleanup written only after the file operations may never be reached. The finally block addresses this problem because it executes after the try and except parts regardless of whether the protected work succeeds or raises an exception.
The Cleanup Guarantee
The finally block is the part of the structure reserved for work that must be attempted after the try and except paths. It runs when the try block succeeds, when an exception is caught by an except block, and when an exception is not caught. This makes it suitable for closing files, releasing locks, and freeing other resources.
The distinction is between outcome-dependent work and cleanup work. Code placed in the normal continuation path is reached only when the protected operation completes without an exception. Code in finally is intended to run regardless of that outcome. It should therefore contain the action that returns the resource from its open state to its closed state.
A Safe File Pattern
A safe pattern prepares the resource variable before entering try. In this example, f starts as None. If opening the file fails immediately, f is still defined and its value shows that no file object was acquired. The finally block checks that value before calling close().
f = None
try:
f = open("records.txt")
for line in f:
process(line)
except IOError:
report_missing_file()
except KeyboardInterrupt:
report_interruption()
finally:
if f is not None:
f.close()- 1f = None creates a safe starting value before any attempt to open the file.
- 2The open operation assigns a file object to f if acquisition succeeds.
- 3The loop performs the dependent reading work.
- 4The except clauses handle the two specific exception paths used in this pattern.
- 5The finally block is reached after success, a handled exception, or an unhandled exception.
- 6The condition ensures close is called only when f refers to an acquired resource.
- The resource variable is initialized before acquisition.
- Opening the file and reading its lines are resource-dependent operations.
- IOError handles the file-related failure described in the source.
- KeyboardInterrupt handles interruption by the user.
- The cleanup block runs after the try or except path.
- The existence check prevents closing a file object that was never acquired.
Tracing a Failed Open
Assume the file cannot be opened and the open operation raises IOError. Which cleanup actions are safe?
- 1Before try
f has the value None, so the cleanup variable already exists even though no file has been acquired.
- 2During acquisition
The open operation raises IOError before assigning a file object to f.
- 3Exception handling
The IOError handler runs and reports the file-related problem.
- 4Cleanup
finally still runs. Its condition is false because f is None, so it does not call close on a nonexistent file object.
The cleanup code completes without creating a second error. Initializing f and checking its value protect the cleanup path when acquisition fails.
Context Handling Around the Block
The topic combines context managers with the with statement, while the supplied mechanism is explained through the finally guarantee. The central resource-handling idea is that setup, dependent work, and cleanup must be treated as one controlled lifecycle. The cleanup part must not depend on the dependent work completing normally.
The important reasoning habit is to trace both outcomes: the normal path and the interrupted path. In either path, identify where cleanup occurs and whether the resource was successfully acquired before attempting to close it.
Mistakes in Cleanup Logic
Common mistake Putting close() only after the file-processing code.
What it looks likeThe program reads the file and calls close() afterward, but an exception occurs during the reading loop.
Why it failsThe exception can bypass the later close call, leaving the file open.
Do this insteadPlace the close operation in finally so cleanup is attempted after the normal and exception paths.
Common mistake Failing to initialize the resource variable before try.
What it looks likeThe code calls close() in finally even though the open operation might fail before assigning the file variable.
Why it failsThe cleanup code may refer to a variable that was never defined, causing an
AttributeError.Do this insteadInitialize the variable to None before try.
Common mistake Closing without checking whether acquisition succeeded.
What it looks likeThe code calls f.close() unconditionally in finally after open() raises IOError.
Why it failsThere is no file object to close when acquisition failed.
Do this insteadCheck that the resource exists before calling close().
Common mistake Treating finally as a success-only branch.
What it looks likeThe code assumes finally runs only when the file loop completes normally.
Why it failsfinally also runs when an exception is caught and in normal cases where an exception is not caught.
Do this insteadUse finally for cleanup that must occur regardless of the execution path.
For resource cleanup, initialize the resource variable before try, acquire the resource inside try, handle the relevant exception paths, and check that the resource exists before closing it in finally.
Trace the Two Paths
Trace a file operation twice: first assuming the file opens and all dependent work succeeds, then assuming an exception occurs during the dependent work. For each trace, identify which part handles the outcome and which part closes the resource.
Hints
- Start with the value of the resource variable before try.
- Ask whether acquisition succeeded before deciding whether close is safe.
- Remember that finally follows both the normal try path and the except path.
What do you think happens?
A file opens successfully, an exception occurs while reading it, and the exception is caught. Does the cleanup code in finally run?
Reveal answer
Yes, finally runs after the except path.
The finally block executes even when an exception is caught, so it can close the file after the interrupted operation.
Key Takeaways
- 1A file is an operating-system resource that enters an open state when acquired.
- 2An exception can bypass cleanup written after resource-dependent work.
- 3The finally block runs after normal execution and exception-handling paths, making it suitable for cleanup.
- 4Initialize resource variables before try and check that a resource exists before closing it.
- 5The cleanup guarantee applies to normal exception-handling situations, with limited exceptions such as forceful termination or a new exception raised inside finally.
At a glance
- Use finally for cleanup that must happen whether protected work succeeds or fails.
- Initialize a resource variable to None before try so cleanup remains safe if acquisition fails.
- Check that the resource exists before calling
close(). - Trace both the success path and the exception path to verify that the resource reaches the closed state.