A NamedTemporaryFile gives you a pathname, but the existence of that pathname is governed by a lifecycle contract, not by the reassuring fact that f.name printed successfully. The operational failure appears when one component creates a temporary file, hands its name to another process, closes the original object, and then assumes the consumer can still open that name. Depending on delete, delete_on_close, context-manager timing, and operating system, that assumption may be correct, predictably false, or dependent on Windows sharing semantics.
The current stable Python documentation, accessed November 6, 2026, identifies itself as Python 3.15.2 documentation. It says that NamedTemporaryFile always has a visible filesystem name, that delete and delete_on_close determine automatic deletion, and that the rules for reopening a still-open file differ between POSIX and Windows. The delete_on_close parameter itself was added in Python 3.12.
That documentation is the contract reference, not evidence about the interpreter installed on a release runner. Acceptance therefore needs both: the documented expectation and a captured execution record from the exact runtime being qualified.
The experiment below stays deliberately narrow. It creates only synthetic files beneath one owned TemporaryDirectory, spawns only child Python processes using the same interpreter, records pathname and file identity state, and destroys its own workspace. No production path, secret, customer record, network service, or external file is accepted as input. The final output is not “the script ran.” It is an evidence ledger that supports one of four operational decisions: ACCEPT, REPAIR, HOLD, or RECONCILE.
Define the acceptance contract and capture the baseline
The acceptance question must be expressed as a timeline, because “can another process open it?” is underspecified without saying when.
There are four materially different moments:
Boundary | Question |
Owner still open | Can a child open the pathname while NamedTemporaryFile remains open? |
Owner explicitly closed, context still active | Does the pathname survive long enough for a child to open it? |
Context exited | Has automatic cleanup removed the pathname? |
Automatic deletion disabled | Can the child reopen it after owner close, and does explicit cleanup then remove it? |
The Python contract separates these cases. With delete=True and delete_on_close=True, both defaults, the file is deleted when it is closed. With delete=True and delete_on_close=False, closing the file object does not immediately remove the pathname; deletion is deferred until the surrounding context exits, or later object finalization if no context manager controls it. With delete=False, automatic deletion is disabled and delete_on_close is ignored.
That leads to a precise post-close acceptance rule:
If another process must open the temporary pathname after the creator has closed its file, the design must not rely on the default delete=True, delete_on_close=True lifecycle. Either use delete=False and own explicit cleanup, or use delete=True, delete_on_close=False and keep the consumer’s reopen inside the context lifetime.
That statement is narrower than “temporary files can be reopened.” Python explicitly distinguishes reopening a still-open file from reopening after close. The documentation recommends delete_on_close=False inside a with statement when the name must be reused after closing, because context exit can still assist with cleanup.
Before intervention, capture the runtime rather than inferring it from the documentation URL:
python -VV
python -c "import os, platform, sys; print(sys.executable); print(sys.version); print(platform.platform()); print(os.name)"The fixture later captures the same information directly into JSON. The evidence fields that matter are at least:
fixture revision
fixture source SHA-256
Python implementation
sys.version
sys.executable
platform.platform()
platform.system()
platform.release()
platform.machine()
os.name
presence of os.O_TEMPORARY
case identifier
owner identity where available
child open result
child bytes
exception type and errno on failure
pathname state after lifecycle boundary
workspace cleanup stateThe source hash matters because a successful run against modified test logic is not evidence for the published protocol. The executable path matters because python in an interactive shell and the interpreter used by a CI runner or service wrapper need not be the same file.
The nearby Refonte article Python Path Containment: Prefix Checks Can Admit Escapes also records interpreter and filesystem evidence, but its core question is different: it asks which object a pathname resolves to under a containment policy. This playbook does not repeat that resolution experiment. Here, every pathname is generated under one trusted temporary root; the disputed property is lifetime and reopenability.
For acceptance, do not treat Path.exists() alone as the authoritative missing-file diagnosis. Current pathlib documentation says Path.exists() returns False for missing, invalid, or inaccessible paths and recommends Path.stat() when those cases need distinguishing. The fixture therefore attempts os.stat() and records the exception class and errno, rather than reducing every failure to a Boolean.
A baseline is acceptable only when the runtime information is complete, the fixture source is fixed by hash, the temporary root is fixture-owned, and no external path can be substituted into the test.
Build a controlled and disposable fixture
The fixture should cross a real process boundary. Opening the pathname twice in one process can confirm some filesystem behavior, but the article’s operational question explicitly concerns another process.
Save the following as namedtemp_reopen_lab.py:
import hashlib
import inspect
import json
import os
import platform
import subprocess
import sys
import tempfile
from datetime import datetime, timezone
from pathlib import Path
PAYLOAD = b"refonte-namedtemp-fixture-v2\n"
CHILD_READ = r'''
import json, os, sys
p = sys.argv[1]
mode = sys.argv[2]
try:
if mode == "builtin":
with open(p, "rb") as f:
data = f.read()
st = os.fstat(f.fileno())
elif mode == "share_delete":
flags = (
os.O_RDONLY
| getattr(os, "O_BINARY", 0)
| os.O_TEMPORARY
)
fd = os.open(p, flags)
try:
data = os.read(fd, 1 << 20)
st = os.fstat(fd)
finally:
os.close(fd)
else:
raise ValueError(mode)
print(json.dumps({
"opened": True,
"bytes": data.decode("ascii"),
"dev": st.st_dev,
"ino": st.st_ino,
}))
except OSError as exc:
print(json.dumps({
"opened": False,
"error": type(exc).__name__,
"errno": exc.errno,
}))
'''
CHILD_HOLD = r'''
import json, sys
p = sys.argv[1]
try:
f = open(p, "rb")
print(json.dumps({"ready": True}), flush=True)
sys.stdin.readline()
data = f.read()
f.close()
print(json.dumps({
"closed": True,
"bytes": data.decode("ascii"),
}), flush=True)
except OSError as exc:
print(json.dumps({
"ready": False,
"error": type(exc).__name__,
"errno": exc.errno,
}), flush=True)
'''
def run_child(path, mode="builtin"):
cp = subprocess.run(
[sys.executable, "-c", CHILD_READ, str(path), mode],
text=True,
capture_output=True,
)
row = json.loads(cp.stdout)
row.update(
returncode=cp.returncode,
stderr=cp.stderr,
)
return row
def owner_id(file_object):
st = os.fstat(file_object.fileno())
return {"dev": st.st_dev, "ino": st.st_ino}
def same(owner, child):
return (
child.get("opened")
and child.get("dev") == owner["dev"]
and child.get("ino") == owner["ino"]
)
def stat_state(path):
try:
st = os.stat(path)
return {
"exists": True,
"dev": st.st_dev,
"ino": st.st_ino,
"size": st.st_size,
}
except FileNotFoundError as exc:
return {
"exists": False,
"error": type(exc).__name__,
"errno": exc.errno,
}
def main():
signature = inspect.signature(tempfile.NamedTemporaryFile)
if "delete_on_close" not in signature.parameters:
raise SystemExit(
"HOLD: NamedTemporaryFile(delete_on_close=...) "
"requires Python 3.12+"
)
report = {
"fixture": "refonte-namedtemp-reopen-v2",
"started_utc": datetime.now(timezone.utc).isoformat(),
"source_sha256": hashlib.sha256(
Path(__file__).read_bytes()
).hexdigest(),
"python": sys.version,
"implementation": platform.python_implementation(),
"executable": sys.executable,
"platform": platform.platform(),
"system": platform.system(),
"release": platform.release(),
"machine": platform.machine(),
"os_name": os.name,
"has_o_temporary": hasattr(os, "O_TEMPORARY"),
"cases": [],
}
with tempfile.TemporaryDirectory(
prefix="refonte-namedtemp-"
) as directory:
root = Path(directory)
report["owned_root"] = str(root)
# Default lifecycle.
with tempfile.NamedTemporaryFile(dir=root) as f:
f.write(PAYLOAD)
f.flush()
path = Path(f.name)
owner = owner_id(f)
child = run_child(path, "builtin")
report["cases"].append({
"id": "default_while_open_builtin",
"owner": owner,
"child": child,
"same_object": same(owner, child),
})
if os.name == "nt":
child = run_child(path, "share_delete")
report["cases"].append({
"id": "default_while_open_share_delete",
"owner": owner,
"child": child,
"same_object": same(owner, child),
})
report["cases"].append({
"id": "default_after_close",
"state": stat_state(path),
"child": run_child(path),
})
# Automatic deletion deferred until context exit.
with tempfile.NamedTemporaryFile(
dir=root,
delete=True,
delete_on_close=False,
) as f:
f.write(PAYLOAD)
f.flush()
path = Path(f.name)
owner = owner_id(f)
f.close()
child = run_child(path)
report["cases"].append({
"id": "deferred_after_owner_close_before_exit",
"owner": owner,
"state": stat_state(path),
"child": child,
"same_object": same(owner, child),
})
report["cases"].append({
"id": "deferred_after_context_exit",
"state": stat_state(path),
"child": run_child(path),
})
# No automatic deletion.
f = tempfile.NamedTemporaryFile(
dir=root,
delete=False,
)
try:
f.write(PAYLOAD)
f.flush()
path = Path(f.name)
owner = owner_id(f)
f.close()
child = run_child(path)
report["cases"].append({
"id": "persistent_after_close",
"owner": owner,
"state": stat_state(path),
"child": child,
"same_object": same(owner, child),
})
finally:
if path.exists():
path.unlink()
report["cases"].append({
"id": "persistent_after_manual_unlink",
"state": stat_state(path),
"child": run_child(path),
})
# Hold an ordinary second open across context exit.
blocker = {
"id": "deferred_exit_with_builtin_blocker"
}
process = None
path = None
try:
try:
with tempfile.NamedTemporaryFile(
dir=root,
delete=True,
delete_on_close=False,
) as f:
f.write(PAYLOAD)
f.flush()
path = Path(f.name)
process = subprocess.Popen(
[
sys.executable,
"-c",
CHILD_HOLD,
str(path),
],
text=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
blocker["child_ready"] = json.loads(
process.stdout.readline()
)
except PermissionError as exc:
blocker["context_exit"] = {
"ok": False,
"error": type(exc).__name__,
"errno": exc.errno,
}
else:
blocker["context_exit"] = {"ok": True}
blocker["state_after_exit_attempt"] = (
stat_state(path)
)
finally:
if process is not None:
try:
process.stdin.write("close\n")
process.stdin.flush()
blocker["child_closed"] = json.loads(
process.stdout.readline()
)
finally:
process.wait(timeout=10)
blocker["child_returncode"] = (
process.returncode
)
blocker["child_stderr"] = (
process.stderr.read()
)
if path is not None and path.exists():
path.unlink()
blocker[
"manual_cleanup_after_blocker"
] = True
else:
blocker[
"manual_cleanup_after_blocker"
] = False
blocker["final_state"] = (
stat_state(path)
if path is not None
else None
)
report["cases"].append(blocker)
never = root / "never-created.bin"
report["cases"].append({
"id": "negative_missing",
"state": stat_state(never),
"child": run_child(never),
})
report["owned_root_removed"] = not Path(
report["owned_root"]
).exists()
report["finished_utc"] = (
datetime.now(timezone.utc).isoformat()
)
print(json.dumps(report, indent=2, sort_keys=True))
if name == "__main__":
main()Run it without redirecting anything into the owned fixture directory:
python -VV
python namedtemp_reopen_lab.py > namedtemp-evidence.json
rc=$?
printf 'fixture_rc=%s\n' "$rc"
python -m json.tool namedtemp-evidence.jsonAn evidence file created by shell redirection is not proof that the Python program succeeded; the shell can create an empty or partial file before the interpreter finishes. Acceptance therefore requires both process status and valid, complete JSON.
The fixture uses TemporaryDirectory because Python documents it as a high-level temporary-resource interface supporting automatic cleanup. That ownership boundary is important: a failed experiment may exercise deletion behavior, so every pathname it can affect must be inside the directory it created.
The source requires Python 3.12 or newer because delete_on_close was introduced in 3.12. An older runtime is not silently converted into a different experiment; it returns HOLD because the intended lifecycle policy cannot be tested with that API.
Record documented behavior before interpreting observations
The evidence ledger should distinguish three labels: documented behavior, predicted observation, and actual observation.
For the default configuration:
tempfile.NamedTemporaryFile(
delete=True,
delete_on_close=True,
)Python says the file is deleted as soon as it is closed. Therefore a child opening the pathname after the owner has closed is predicted to fail because the directory entry should no longer exist.
For:
tempfile.NamedTemporaryFile(
delete=True,
delete_on_close=False,
)Python says the file is deleted on context-manager exit rather than on ordinary close(). Therefore a sequence of owner close → child reopen → child close → context exit is the intended portable pattern when a named temporary file must be reopened after the owner closes but automatic context cleanup is still desired.
For:
tempfile.NamedTemporaryFile(
delete=False,
)the delete_on_close setting is ignored. The pathname should survive both close and context exit until application code removes it. That gives the consumer the broadest reopen window, but cleanup responsibility moves fully into the application.
The article Python tarfile Errors: Validate Staged Extraction makes a related evidence distinction (API completion is not equivalent to completed downstream effects), but its state machine concerns archive members already extracted before a later rejection. This fixture does not extract, stage, or publish anything; it tests only named temporary-file lifecycle transitions.
The most important platform split occurs while the owner is still open.
Python’s current tempfile documentation says a NamedTemporaryFile can always be opened again by name while still open on POSIX. On Windows, at least one of several conditions is required: delete=False; the additional open shares delete access, with os.open(..., os.O_TEMPORARY) given as an example; or delete=True, delete_on_close=False.
That distinction is grounded in Windows handle-sharing rules rather than in Python pathname syntax. Microsoft’s CreateFile documentation says an existing open handle constrains subsequent opens according to its sharing mode, and that sharing options remain in force until the handle closes. FILE_SHARE_DELETE is specifically the sharing permission relevant when delete access is involved.
Python exposes os.O_TEMPORARY only on Windows and points readers to the platform C runtime documentation for these flags. Microsoft documents OTEMPORARY as creating/opening a temporary file that is deleted when the last corresponding descriptor closes; Python’s tempfile documentation specifically cites os.O_TEMPORARY as a way for a second Windows open to share delete access.
The false-positive hypothesis is therefore:
A test may report “child successfully opened the temporary filename,” while failing to test the lifecycle boundary the application actually depends on.
On POSIX, a successful child open while the original handle is still open tells you almost nothing about whether a child can open the pathname after the owner closes. Under the default deletion policy, those two events are expected to produce opposite outcomes.
Likewise, testing only delete=False can produce an apparently healthy reopen result while entirely bypassing the automatic cleanup policy used by the application.
The acceptance fixture must therefore preserve case labels. Do not collapse every successful child read into one reopen_ok=True.
Reproduce the misleading success signal and run the negative controls
The first case deliberately asks the easiest question:
create NamedTemporaryFile with defaults
write and flush synthetic payload
keep owner open
spawn child
child calls builtin open(path, "rb")On POSIX, Python documents that this reopen is allowed. A successful result is therefore a useful positive control, but it is not evidence that the pathname survives owner close.
A real preparation run of the published fixture was captured on November 6, 2026 in a disposable Linux environment. The runtime recorded itself as:
CPython
3.13.5 (main, Jul 15 2026, 20:25:40) [GCC 14.2.0]
/opt/pyvenv/bin/python
Linux-6.18.57-x86_64-with-glibc2.41
os.name = posixThe fixture source SHA-256 for that run was:
0f8b8a855f0a5860d0658058d17ad5f1ff3fbb1efd71340feac20754750a5515This is preparation evidence for that exact local runtime, not a claim about Windows or about another reader’s deployment.
The observed Linux case behaved as predicted:
{
"id": "default_while_open_builtin",
"child": {
"opened": true,
"bytes": "refonte-namedtemp-fixture-v2\n"
},
"same_object": true
}The same run then crossed the boundary that matters:
leave the default NamedTemporaryFile context
automatic close occurs
spawn a fresh child
child attempts builtin open(path, "rb")The captured result was FileNotFoundError, errno 2, while os.stat() also reported the path missing. That is the required contrast: the earlier successful reopen did not imply reopenability after default close.
The second negative control uses delete=True, delete_on_close=False:
create file
write and flush
capture owner identity
explicitly close owner
remain inside with block
spawn childThe preparation run observed that the child reopened the pathname and read the expected bytes. After context exit, a fresh child received FileNotFoundError. That sequence matches the documented division between close-time deletion and context-exit deletion.
The third branch uses delete=False. In the captured Linux run, the child reopened successfully after owner close. The fixture then explicitly unlinked only its owned file, and a second child failed with FileNotFoundError. The successful reopen and successful removal are separate assertions.
That separation resembles the evidence discipline in CloudFront Cache Isolation: Audit Personalized Responses: a positive response alone does not establish that the intended isolation boundary held. The CloudFront article uses synthetic identities and cache state; this article uses synthetic bytes and file lifecycle state. They are not interchangeable experiments.
A fourth case deliberately holds an ordinary child open() across the parent context’s cleanup attempt. This is the platform discriminator.
Python documents that on Windows, when delete=True, delete_on_close=False, an additional open that does not share delete access must be closed before the NamedTemporaryFile context exits; otherwise its os.unlink() cleanup can raise PermissionError.
The captured Linux run showed the opposite lifecycle:
{
"id": "deferred_exit_with_builtin_blocker",
"context_exit": {"ok": true},
"state_after_exit_attempt": {
"exists": false,
"error": "FileNotFoundError",
"errno": 2
},
"child_closed": {
"closed": true,
"bytes": "refonte-namedtemp-fixture-v2\n"
}
}That is consistent with Python’s os.remove/unlink documentation: on Unix, removing an in-use file removes the directory entry, while the storage persists until the existing open reference is no longer in use. On Windows, removal of an in-use file can instead raise an exception.
The final negative control attempts a child open on never-created.bin under the same temporary root. Its purpose is mundane but important: if the harness unexpectedly reports success for a path that was never created, trust in every positive case collapses. A release test without a deliberately impossible control can accidentally validate its own parser, stale file, or case-selection mistake instead of the API behavior.
Measure identity, lifecycle, and completed effects
A pathname string is not enough evidence. The fixture therefore records identity information from the open owner and from the child’s successful open.
For each local successful case, it captures:
st = os.fstat(fd)
{
"dev": st.st_dev,
"ino": st.st_ino,
}On the captured Linux run, the owner and child reported matching device/inode pairs in the successful cases. That is supporting evidence that the child read the object the owner created rather than merely finding some file at the same spelling.
This comparison should remain diagnostic rather than be generalized into a cross-platform universal object-identity API. The acceptance claim does not need that generalization. The required operational facts are simpler: the child opened the fixture pathname at the declared lifecycle boundary and read the exact expected synthetic payload.
The payload check matters because an open() return alone proves only that something was opened. The fixture uses a fixed, inert byte string:
refonte-namedtemp-fixture-v2\nA qualifying successful child record should therefore contain both:
"opened": trueand the expected payload. Identity metadata strengthens the record where meaningful.
Likewise, a deletion result should not be inferred merely because a context manager exited without raising. The fixture independently probes the pathname afterward.
That is the difference between output and completed effect:
"with block finished"is execution output.
fresh os.stat(path) -> FileNotFoundError
fresh child open(path) -> FileNotFoundErroris independent evidence about the pathname after cleanup.
The same distinction applies to failures. A PermissionError raised during Windows context exit would not mean “the file is safely preserved” in some application-defined sense. It means the cleanup operation did not complete as expected. The fixture must then record the actual pathname state, close the owned blocker, perform only the fixture’s scoped recovery cleanup, and retain both the original failure and recovery actions.
Microsoft’s file-deletion documentation is relevant here. Windows can defer deletion until handles close, and sharing modes determine whether deletion-related operations are permitted while handles remain open. CreateFile also documents FILE_FLAG_DELETE_ON_CLOSE in terms of all handles and FILE_SHARE_DELETE.
That is why a Windows acceptance record should not be summarized as “Windows locks files.” That phrase hides the material distinction between access rights, share modes, delete access, deletion pending state, and which handles remain open.
Independent observation should also survive cleanup. The JSON evidence file belongs outside the temporary root. If the evidence were written inside the directory whose cleanup is itself under test, a correct cleanup operation could erase the material needed to diagnose a failed case.
One more subtle boundary deserves explicit treatment: flush() is required before the child is asked to read bytes written through the owner’s buffered file object. This fixture calls flush() before every child read. It does not claim that flush() provides durable media persistence after a power loss; that is outside this playbook. The property being tested is process-visible file content and pathname lifecycle, not crash-consistent storage.
Collect the evidence ledger, repair the boundary, and reconcile recovery
Treat each case as an evidence row, not as console prose.
A practical ledger can normalize the JSON into this shape:
Case | Policy/timeline | Required observation | Failure interpretation |
default_while_open_builtin | defaults, owner still open | POSIX: child opens; Windows behavior governed by share rules | Unexpected result or unsupported assumption |
default_after_close | defaults, owner closed | path absent; fresh child fails | Default lifetime does not match post-close handoff |
deferred_after_owner_close_before_exit | delete_on_close=False, owner closed, context active | fresh child opens and reads payload | Candidate repair failed |
deferred_after_context_exit | same case after context exit | path absent; child fails | Automatic cleanup incomplete |
persistent_after_close | delete=False | child opens after owner close | Explicit-lifetime design failed |
persistent_after_manual_unlink | after application cleanup | path absent; child fails | Manual cleanup failed |
deferred_exit_with_builtin_blocker | second ordinary open held over cleanup | POSIX and Windows diverge according to documented semantics | Platform assumption must be reconciled |
negative_missing | never-created path | child fails | Harness cannot discriminate missing paths |
A related comparison is HTTP Cache Revalidation: Validate Conditional Requests, which likewise warns against treating a successful protocol-level response as proof of the completed property being accepted. Its evidence is HTTP representation state; this ledger is strictly local filesystem lifecycle evidence.
Repair begins with the application requirement.
If the consumer must receive a pathname after the producer closes, the default policy is the wrong boundary. The narrow repair is:
with tempfile.NamedTemporaryFile(
delete=True,
delete_on_close=False,
) as tmp:
tmp.write(payload)
tmp.flush()
path = tmp.name
tmp.close()
# Invoke the consumer here.
subprocess.run(
["consumer", path],
check=True,
)
# Automatic unlink is attempted here.That design keeps the pathname alive between owner close and context exit, then returns cleanup responsibility to the context manager. It is appropriate only when the consumer completes before the context exits.
On Windows, ordinary additional opens must also be closed before context exit if they do not share delete access; Python documents that otherwise the context’s os.unlink() can fail with PermissionError.
If the consumer outlives the producer’s context, use an explicitly owned persistent temporary file instead:
tmp = tempfile.NamedTemporaryFile(
delete=False,
)
try:
tmp.write(payload)
tmp.flush()
path = Path(tmp.name)
tmp.close()
run_consumer(path)
finally:
try:
path.unlink()
except FileNotFoundError:
passThe crucial change is not merely delete=False. It is the transfer of cleanup ownership into explicit application code. The evidence ledger should identify that owner and verify cleanup after the consumer completes.
Do not “repair” the failing test by adding sleeps. The disputed state transition is not a scheduling delay. Under the default policy the pathname is intentionally deleted on close; waiting longer cannot make the documented lifecycle more suitable.
Do not repair by passing the already-open file descriptor to a test that is supposed to qualify pathname reopening. Descriptor inheritance is a different contract.
Do not repair by catching FileNotFoundError and creating a replacement file at the same name. That would turn a lifecycle test into a different-object test.
After repair, run the same fixture logic unchanged around the application’s intended policy. A successful recovery run must preserve the original failed evidence; otherwise the team loses the distinction between “the boundary was violated and repaired” and “the boundary was never tested.”
Use RECONCILE when cleanup has partially executed or its final state is uncertain. A Windows PermissionError on context exit is a good example: close only the fixture-owned conflicting handle, inspect whether the pathname remains, remove the owned file if needed, and record those recovery actions. Never erase the first failure from the ledger merely because manual cleanup later succeeded.
Python’s contextlib model supports this general cleanup discipline: context managers release resources on block exit, and ExitStack can compose cleanup callbacks in reverse acquisition order. That is useful if a larger acceptance fixture acquires several owned resources, although NamedTemporaryFile itself already supports the context-manager protocol and needs no wrapper merely for style.
Build the release-gate decision matrix and document limits
A release decision should be derived from the required application timeline, not from whichever case happened to turn green.
Decision | Evidence condition | Operational meaning |
ACCEPT | Required target-platform cases match documented expectations; payload and lifecycle probes agree; cleanup succeeds | The selected policy fits the declared handoff |
REPAIR | Failure has a known local cause and a bounded policy change is available | Change lifecycle policy, then rerun unchanged acceptance cases |
HOLD | Runtime unsupported, platform not executed, contradictory evidence, incomplete ledger, unexpected exception, or cleanup state uncertain | Do not release the assumption |
RECONCILE | Test or recovery performed partial filesystem effects requiring inspection and controlled cleanup | Establish final owned state before retesting |
For a service that creates a temporary file, closes it, launches another process by pathname, waits for that process, and then wants automatic cleanup, the expected accepted configuration is typically delete=True, delete_on_close=False with the consumer entirely inside the context lifetime. That recommendation follows directly from Python’s documented post-close reuse guidance; it is not a general statement that this option is best for every temporary-file workflow.
For a job that needs the pathname to survive beyond the creator’s context, ACCEPT should instead require delete=False plus explicit, evidenced application cleanup.
For an application using the defaults and expecting a child to open the pathname only after producer close, the correct decision is REPAIR, not “intermittent filesystem issue.” The policy and the requirement contradict one another.
For Windows, do not grant cross-platform acceptance from a Linux run. The current Python documentation explicitly imposes Windows-specific conditions on concurrent reopening and warns that an ordinary secondary open can block cleanup when delete_on_close=False. Microsoft’s sharing documentation independently describes the underlying handle-sharing constraints.
A Linux-only record therefore supports only Linux acceptance. A Windows target remains HOLD until the same source revision runs on the exact supported Windows/Python combination and its evidence is retained.
The release gate should also reject these shortcuts:
"CI was green."No. Identify the case rows.
"The child exited zero."No. The child helper deliberately serializes open failures into JSON, so its process status alone is not the oracle.
"The filename existed earlier."No. Record state at the relevant boundary.
"It worked on Unix."No. Windows has different documented sharing rules.
"We manually deleted the leftover."That may complete reconciliation; it does not retroactively turn failed automatic cleanup into success.
The article PostgreSQL SKIP LOCKED: Test Queue Fairness and Starvation applies a similar release-gate principle to a completely different primitive: one successful concurrency property does not establish every operational property a system needs. Here, a successful temporary-file open at one moment does not establish reopenability at another lifecycle boundary.
Several limits should remain explicit.
This fixture does not qualify network filesystems, shared remote volumes, antivirus interference, container bind mounts, unusual Windows filter drivers, abrupt machine failure, SIGKILL cleanup, privilege transitions, or applications that deliberately manipulate handles through native APIs. Python separately notes that on POSIX an abrupt SIGKILL prevents automatic deletion of NamedTemporaryFile objects created by the terminated process.
It does not test confidentiality of temporary-file contents, directory permission design, filename predictability attacks, symlink attacks, durable persistence, or secure deletion. NamedTemporaryFile creation and reopen lifecycle are the only acceptance target.
It also does not convert a local preparation run into a universal result. The preparation evidence captured CPython 3.13.5 on Linux 6.18.57. The documentation consulted was the current Python 3.15.2 library documentation on November 6, 2026. A release owner should retain the documentation version consulted and the runtime version actually executed as two separate fields.
That distinction is especially important over time: delete_on_close was added in Python 3.12, so older articles or remembered behavior from Python 3.11 may describe a materially different API surface.
Apply the evidence discipline with Refonte Learning System Administration
The operational lesson is not “memorize one tempfile flag.” It is to qualify the exact state transition that a release depends on.
For NamedTemporaryFile, the useful acceptance chain is:
declare required reopen timeline
capture exact runtime
create only fixture-owned state
write and flush known bytes
cross a real child-process boundary
observe pathname and content independently
cross the close/context boundary
probe again
retain failures as evidence
perform only scoped recovery
derive ACCEPT / REPAIR / HOLD / RECONCILEThat sequence makes the difference between three statements visible:
The file had a name.
The file was openable at time T.
The pathname remained openable after lifecycle event L.Only the third statement answers the release question when a producer closes before handing work to a consumer.
The current Python contract is sufficiently precise to predict the principal outcomes: default close removes the file; delete_on_close=False can defer deletion to context exit; delete=False leaves cleanup to the application; POSIX permits a second open while the first remains open; and Windows adds sharing constraints that must be validated on Windows.
The fixture then turns those statements into falsifiable local evidence. Its positive controls prove that the child mechanism can read a known file. Its negative controls prove that missing pathnames are actually detected. Its owner-close and context-exit cases distinguish the two deletion boundaries. Its held-child case forces the POSIX/Windows cleanup difference into an observable timeline instead of hiding it behind general statements about file locking.
For release acceptance, that is enough. There is no need to widen the procedure into a general temporary-file tutorial.
Refonte Learning’s System Administration program currently lists Windows and Linux systems, troubleshooting techniques, system installation and configuration, command-line work, virtualization, backup and recovery among its stated areas. The page also currently lists a six-month period and a weekly dedication estimate of 10 to 12 hours. The program page does not specify whether this NamedTemporaryFile fixture or Python API is taught.
For engineers using that broader systems context, this playbook is one concrete exercise in the same discipline: define the operating boundary, capture the actual platform, reproduce the failure with owned state, preserve contradictory evidence, and refuse promotion until the lifecycle observed at the release boundary matches the lifecycle the application requires.
