Why does `ResourceWarning: unclosed file` appear only under `-X dev`?
answer
- It was never missing, only hidden
- Two default behaviours combine here
- The message comes from an object's last moment
- Reported line is where it died
- Another startup switch supplies the birth site
basics
~20 sResourceWarning is discarded by CPython's default warning filters, so nothing prints. Development mode adds the default filter, which shows it. The warning itself is emitted by the file object's finalizer when it is garbage-collected without being closed.
solid answer
~50 sTwo separate things have to line up. First, **visibility**: `ResourceWarning` is one of the categories CPython's default filters ignore, so a leaked file handle is silent until something un-ignores it -- and `-X dev` does exactly that by adding the `default` filter action. Second, **emission**: the warning comes from the file object's *finalizer*, so it fires when the object is destroyed, not when it is opened. Under CPython's reference counting that is usually the moment the last name goes away, but an object caught in a reference cycle waits for a garbage-collection pass, and during interpreter shutdown the warning may never be printed at all. That is why the reported location is where the object died, not where it was created; pairing dev mode with `-X tracemalloc=5` attaches the allocation traceback. The fix is to own the lifetime with a `with` block.
code
python · 12 linesimport tempfile
with tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) as tmp:
tmp.write(b"0000ftypmp42")
def read_brand(path):
handle = open(path, "rb") # never closed
return handle.read(12)[8:]
print(read_brand(tmp.name))go deeper
Remember that ResourceWarning is hidden by CPython's default warning filters and that a with block is the fix. Know that -X dev is what makes leaked-handle warnings show up at all.
Explain both halves: the category is ignored by default and un-ignored by development mode, and the message is emitted by the object's finalizer, which is why it arrives late and reports the wrong line.
Demonstrate the diagnosis loop -- pair dev mode with -X tracemalloc=5 to get allocation tracebacks, reason about cycles and shutdown ordering, and treat silence as weak evidence rather than proof.
Decide the policy: whether a leaked-handle warning is allowed to fail a build, how long-lived services are audited for descriptor exhaustion, and where the cost of allocation tracking is worth paying.
### The category is ignored by default `ResourceWarning` is the category CPython uses to say *you let an operating-system resource go without releasing it explicitly*: an unclosed file object, an unclosed socket, a `subprocess.Popen` still running at collection time, an event loop that was never closed. It is in the group of categories CPython's out-of-the-box warning filters discard outright, together with `PendingDeprecationWarning`, `ImportWarning`, and `DeprecationWarning` triggered outside `__main__`. That default is deliberate rather than careless. These warnings fire from finalizers, at moments the application author does not control, and often from inside library code the user cannot fix. Printing them at every end user would be noise. So CPython hides them and gives you a switch to see them again -- and development mode is that switch: `-X dev` adds a filter whose action is `default`, meaning each warning is printed once per unique source location. Nothing about the warning changed; only whether it is shown. ### Where the warning comes from The second half of the answer matters more in practice. The warning is emitted by the **file object's finalizer**, the code that runs when the object is destroyed. That has three consequences. **The location is the death site, not the birth site.** The traceback you get points at whatever line happened to drop the last reference -- frequently a function return, a loop iteration boundary, or nothing recognisable at all. Engineers reading their first `ResourceWarning` often chase the wrong function entirely. **When it fires depends on how the object died.** CPython frees an object as soon as its reference count hits zero, so in the common case the warning arrives promptly, right where the last name went out of scope. But if the file is reachable from a reference cycle -- stored on an object that points back at itself, captured by a closure held on a traceback -- it survives until a cycle-collection pass runs, and the warning arrives arbitrarily later, in an unrelated part of the program. This is also a portability point: implementations without reference counting collect much later, so code that leaks handles and "works fine" on CPython can exhaust the process descriptor limit elsewhere. **At interpreter shutdown you may see nothing.** Finalization ordering during teardown is not guaranteed, and objects still alive when the machinery that formats and prints warnings is being dismantled can be reclaimed without any message. So *absence* of a `ResourceWarning` under dev mode is weak evidence: it says a warning was not printed, not that every handle was closed. ### Getting the allocation site Because the death site is rarely the useful location, the standard recipe is to run development mode together with allocation tracking: ```console $ python -X dev -X tracemalloc=5 extract.py ``` With `tracemalloc` enabled, the `ResourceWarning` carries the traceback of where the leaked object was **allocated** -- the `open()` call itself, up to the requested number of frames. That is the line you actually have to change. `PYTHONTRACEMALLOC=5` does the same from the environment. Note the cost asymmetry: dev mode is cheap-ish, `tracemalloc` is not, so this pairing belongs in a reproduction or test run rather than a permanent setting. ### The fix The warning is a real defect report, not interpreter chatter. The remedy is almost always to make the lifetime explicit rather than to silence the category: * Wrap the handle in a `with` block, so it closes on every path including exceptions. * When a function must hand a live handle back to a caller, document that the caller owns closing it -- or return the bytes instead of the handle. * For a set of handles opened dynamically, use `contextlib.ExitStack` so each one is registered for closing as it is created. * For sockets and subprocesses, the same rule applies -- `socket.socket` supports the context-manager protocol, and a `subprocess.Popen` needs its `wait()` (or its own `with` block) so the child is reaped. An interviewer is usually checking two things with this question: that you know the category is hidden rather than absent, and that you understand the warning is produced by a finalizer -- which is why it is late, why its location misleads, and why it sometimes fails to appear at all.
- The traceback on a `ResourceWarning` points at a line that never opens a file. Why?Because the warning is emitted by the object's finalizer, so the reported frame is wherever the last reference was dropped -- a function return, a loop boundary, or a garbage-collection pass if the object sat in a reference cycle. It is the death site, not the `open()` call. Enable `tracemalloc` alongside development mode and the warning carries the allocation traceback instead.
- You run a suite under `-X dev`, see no `ResourceWarning`, and conclude nothing leaks. What is wrong with that?The warning only fires when the object is actually finalized while the warning machinery still works. Objects held alive to the end of the process -- in module globals, caches, or reference cycles -- may be reclaimed during interpreter shutdown with no message at all. Silence means no warning was printed, not that every handle was closed; audit ownership rather than trusting the absence.
- Besides files, what else raises `ResourceWarning`?Any object holding an operating-system resource that was not released explicitly: an unclosed `socket.socket`, a `subprocess.Popen` whose child is still running when the object is collected, an unclosed asyncio event loop, memory-mapped files. The same rules apply -- hidden by default, emitted from a finalizer, fixed by owning the lifetime with a context manager or an explicit close.
saying these in an interview costs you the question
- Thinks the warning does not exist unless dev mode creates it
- Says the traceback points at the open() call
- Believes CPython closes leaked files, so nothing is wrong
- Silences the category instead of fixing the lifetime
- Assumes no warning printed proves no handle leaked
- Confuses dev mode with tracemalloc allocation tracking