Resource Cleanup
Resource Cleanup
Every open file, lock, and temp directory needs a matching close. try / finally is the primitive. with is the default. ExitStack is the tool when the number of managers is not known until runtime. The boring default is with, plus tempfile.TemporaryDirectory so examples (and tests) leave no files behind.
Mental model
If you acquire it, you release it on every path: success, return, and exception.
try / finally is explicit:
f = path.open("w")
try:
f.write(...)
finally:
f.close()with path.open("w") as f: is the same pairing, shorter, and harder to skip. Prefer it.
contextlib.ExitStack is a stack of context managers. enter_context(cm) calls __enter__ now and schedules __exit__ for when the stack unwinds. callback(fn) schedules a function. Use it for “open N shift files” where N comes from a list.
tempfile.TemporaryDirectory and NamedTemporaryFile create paths that vanish when the with ends. Do not write tutorial files next to the script.
Worked examples
Case 1: try / finally
Save as try_finally_file.py. The close happens even if write raised. We still use a temp directory so the path is not a leftover.
# try_finally_file.py
from pathlib import Path
import tempfile
def main():
with tempfile.TemporaryDirectory() as d:
path = Path(d) / "shift.txt"
f = path.open("w")
try:
f.write("open 4\n")
finally:
f.close()
print(path.read_text().strip())
print(f.closed)
if __name__ == "__main__":
main()Run:
uv run python try_finally_file.pyOutput:
open 4
True
This is what you write when the object is not a context manager. For a file, it is the long spelling of with.
Case 2: the same job with with
Save as with_file.py. One block, close is implied.
# with_file.py
from pathlib import Path
import tempfile
def main():
with tempfile.TemporaryDirectory() as d:
path = Path(d) / "shift.txt"
with path.open("w") as f:
f.write("open 4\n")
print(path.read_text().strip())
if __name__ == "__main__":
main()Run:
uv run python with_file.pyOutput:
open 4
After the inner with, f is closed. path.read_text() opens and closes its own handle. Nested with on the directory plus the file is the usual shape.
Case 3: ExitStack for a dynamic set of files
Save as exit_stack.py. Three shift files, one stack. All three close when the stack’s with ends — even if the second write failed.
# exit_stack.py
from contextlib import ExitStack
from pathlib import Path
import tempfile
def main():
with tempfile.TemporaryDirectory() as d:
root = Path(d)
names = ("open", "mid", "close")
with ExitStack() as stack:
files = []
for name in names:
path = root / f"{name}.txt"
f = stack.enter_context(path.open("w"))
f.write(f"{name} shift\n")
files.append(path.name)
print(len(files))
print((root / "mid.txt").read_text().strip())
if __name__ == "__main__":
main()Run:
uv run python exit_stack.pyOutput:
3
mid shift
Do not nest with open(...) three levels deep because you happen to have three names. Do not keep a list of handles and close them in a loop you might not reach.
Case 4: a callback when there is no context manager
Save as exit_callback.py. stack.callback(f.close) is finally: f.close() on the stack.
# exit_callback.py
from contextlib import ExitStack
from pathlib import Path
import tempfile
def main():
with tempfile.TemporaryDirectory() as d:
path = Path(d) / "shift.txt"
with ExitStack() as stack:
f = path.open("w")
stack.callback(f.close)
f.write("open 4\n")
print(f.closed)
print(path.read_text().strip())
if __name__ == "__main__":
main()Run:
uv run python exit_callback.pyOutput:
True
open 4
Prefer enter_context(path.open("w")) for files. Use callback for a close function you already have.
A helper that only needs one path can stay tiny:
# named_temp.py
from pathlib import Path
import tempfile
def write_shift(path, name, hours):
path.write_text(f"{name} {hours}\n")
return path.read_text().strip()
def main():
with tempfile.TemporaryDirectory() as d:
path = Path(d) / "shift.txt"
print(write_shift(path, "open", 4))
if __name__ == "__main__":
main()Run:
uv run python named_temp.pyOutput:
open 4
write_text / read_text close internally. The directory still needs the with so the folder goes away.
The trap
Opening files in a loop and hoping CPython closes them when the function returns — handles = [path.open("w") for path in paths] — is a leak. The list holds the handles alive. An exception before you close them leaves them open. On some systems you run out of file descriptors before you run out of patience.
The other trap is a hard-coded path (/tmp/shift.txt) in a book example or a test. The next run collides. A crashed run leaves the file. TemporaryDirectory is the whole fix.
try / finally that calls close but forgets it on the success path (close only in except) is the same leak. finally or with, not except alone.
The boring rule
- Prefer
withovertry/finallywhen the object supports it. - Use
try/finallyfor acquire/release pairs that are not context managers. - Use
ExitStackwhen the count of managers is dynamic. - Use
tempfile.TemporaryDirectory(orNamedTemporaryFile) in programs and tests that need a path. Do not leave files in the working directory. - Close in reverse order of open.
ExitStackdoes that. - Do not rely on garbage collection to close files or sockets.
Try this
- In
try_finally_file.py, raiseValueError("full")afterwrite, catch it outside thetry/finally, and printf.closed— it should still beTrue. - Rewrite
exit_callback.pyto useenter_contextinstead ofcallback. Keep the prints. - Add a fourth name
"bar"to thenamestuple inexit_stack.py. Confirmlen(files)is4. - In
named_temp.py, write two files (open.txt,mid.txt) in the same temp directory and print both.