Watch at EaseStories. Answers. Perspective.

Technology · PRACTICAL GUIDE

NamedTemporaryFile works on POSIX but fails on Windows: choose the reopen and deletion contract

A temporary path has three owners: the creator, the second opener, and cleanup. Windows share modes force you to decide which handle stays open before you pass the name.

Original diagram showing creator, consumer, and cleanup as three owners of a named temporary file.
Original illustration: bind the creator, consumer, and cleanup responsibilities before passing a path. Watch at Ease

NamedTemporaryFile gives you a visible path, but that path is not a promise that every second handle can open, rename, or delete it. POSIX permits a new open while the original remains open; Windows checks every existing handle’s access and share modes. A workflow that passes on Linux can therefore raise PermissionError or a sharing violation on Windows.

Choose the lifetime contract before creating the file: will the consumer receive an already-closed path, will it open while the creator remains open, or will it own cleanup? Then bind delete and delete_on_close to that choice, close incompatible handles before context exit, and verify both consumer success and final deletion.

Name the next owner before creating the file

Write down whether the next user is the same Python handle, a second Python open, a library that accepts a path, or an external process. Also state whether that consumer needs only read/write access or may rename, replace, or keep the file mapped.

If the consumer accepts a file object, prefer passing the existing object and avoid a second pathname open. If it requires a path, the handle and cleanup contract must be explicit.

Choose one of three lifetime contracts

For same-context reopen on Python 3.12+, delete=True with delete_on_close=False keeps automatic context-exit cleanup while allowing an ordinary second open on Windows. Close that second handle before leaving the context.

For an external process or API that cannot share delete access, flush and close the producer, use delete=False, pass the path, wait for the consumer, then remove the file in finally. For a specialized second Python open that shares delete access, use the documented O_TEMPORARY route only when every participant follows the same contract.

Do not confuse close, flush, and process completion

flush makes Python buffers visible but does not release the Windows handle or its share-mode record. close releases that handle. Waiting for the subprocess proves only that process exit occurred; it does not prove another library or mapped view released the file.

Keep the producer, consumer, and cleanup timestamps in a small ledger. A cleanup PermissionError is evidence of an outstanding handle or missing delete right, not a reason to silently leak the path.

Treat rename and delete as their own access class

Microsoft documents that delete access includes rename. A file can be readable and writable yet still reject os.replace, fileinput in-place editing, or cleanup because a prior handle did not share delete access.

Do not diagnose every WinError 32 as another process. Record the exact operation, every handle you own, whether each is closed, and whether the consumer might retain a mapping.

Verify the contract on both platforms

Create a fixture that writes bytes, reopens or hands off the path exactly as production does, confirms the consumer read the expected hash, closes every handle, exits the context, and asserts whether the path should exist. Run it on Windows and POSIX.

For delete=False, assert deletion in finally and fail loudly if removal fails. For delete_on_close=False, assert the second handle closes before context exit. Also test the exception path so cleanup is not dependent on a successful consumer.

Original diagram comparing incompatible and compatible Windows file sharing contracts.
Original illustration: Windows validates access against every open handle and its share modes. Watch at Ease