Resource Cleanup

Updated

September 8, 2026

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

Output:

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

Output:

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

Output:

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

Output:

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

Output:

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 with over try / finally when the object supports it.
  • Use try / finally for acquire/release pairs that are not context managers.
  • Use ExitStack when the count of managers is dynamic.
  • Use tempfile.TemporaryDirectory (or NamedTemporaryFile) in programs and tests that need a path. Do not leave files in the working directory.
  • Close in reverse order of open. ExitStack does that.
  • Do not rely on garbage collection to close files or sockets.

Try this

  1. In try_finally_file.py, raise ValueError("full") after write, catch it outside the try / finally, and print f.closed — it should still be True.
  2. Rewrite exit_callback.py to use enter_context instead of callback. Keep the prints.
  3. Add a fourth name "bar" to the names tuple in exit_stack.py. Confirm len(files) is 4.
  4. In named_temp.py, write two files (open.txt, mid.txt) in the same temp directory and print both.