How do you make CPython report unclosed files as ResourceWarning during tests?
answer
- The interpreter already noticed
- A warning class nobody sees
- One startup flag flips the filters
- Add allocation tracebacks to make it actionable
- Fires from the finalizer, so beware error mode
basics
~20 sCPython already emits ResourceWarning when a file, socket or pipe is deallocated while still open, but that warning is ignored by default. Run with -X dev or PYTHONDEVMODE=1 to surface it, and add -X tracemalloc to get the allocating line.
solid answer
~40 sWhen a file object, socket or pipe is deallocated with its descriptor still open, its finalizer emits a `ResourceWarning` naming the object. You normally never see it because `ResourceWarning` is in the interpreter's default ignore filters. Development mode turns it on: `python -X dev script.py`, or `PYTHONDEVMODE=1` in the environment, installs the `default` warning filter (plus `faulthandler`, allocator debug checks and asyncio debug mode). Add `-X tracemalloc=25` and the warning carries the object's allocation traceback, so it names the exact line that opened the descriptor rather than only what leaked. For narrower control, `-W default::ResourceWarning` enables just this warning and `-W error::ResourceWarning` promotes it — though because it fires from a finalizer, that surfaces through `sys.unraisablehook` rather than as a clean traceback. Inside a test, `warnings.catch_warnings(record=True)` with `warnings.simplefilter` asserts on it directly.
code
console · 1 linepython3 -X dev -X tracemalloc=5 -c "f = open('/etc/hosts'); del f"go deeper
Recall that Python has a warning specifically for unclosed files and that it is hidden by default. Knowing the phrase development mode and the -X dev flag is enough at this level.
Explain the filter model: ResourceWarning is in the default ignore list, -X dev or PYTHONDEVMODE installs the default filter, and -X tracemalloc attaches the allocation traceback. Know how to scope it with warnings.catch_warnings in a test.
Show judgement about where these switches live: dev mode in CI and local runs, not production, plus a descriptor gauge for long-lived processes because finalizer-time warnings never fire for a leak that is still referenced.
Own it as a standard: development mode wired into the default test command across services so a reintroduced leak fails in CI, and a decision on what the production signal is instead, since you will not ship debug allocators to a hot path.
### Why the leak is silent by default CPython already detects unclosed files: when a file object, socket or pipe is deallocated while its descriptor is still open, its finalizer emits a `ResourceWarning` such as `unclosed file <_io.TextIOWrapper name='/etc/hosts' ...>`. You almost never see it, because `ResourceWarning` sits in the interpreter's default ignore filters — alongside `DeprecationWarning` and `PendingDeprecationWarning`. The reasoning is that these warnings are aimed at the author of the code, not at the end user of an application, so the default build stays quiet. The consequence is that a descriptor leak is invisible right up to the moment the process hits its descriptor ceiling and `OSError` (*Too many open files*) starts coming out of unrelated code. ### Development mode is the switch `python -X dev script.py`, or the equivalent `PYTHONDEVMODE=1` environment variable, turns on CPython's development mode (added in 3.7 and unchanged through 3.14). Among other things it installs the `default` warning filter, so `ResourceWarning` — and each other normally-hidden warning — is printed once per location. Development mode also enables `faulthandler`, debug checks in the memory allocators, and asyncio's debug mode; it does **not** make the process meaningfully slower for the purposes of a test suite, and it deliberately does not change the behaviour of your code, only what it reports. Related switches worth knowing: * `-W default::ResourceWarning` (or `PYTHONWARNINGS=default::ResourceWarning`) turns on just this warning, without the rest of dev mode. * `-W error::ResourceWarning` promotes it to an exception. Be aware where it is raised: the warning comes from the object's finalizer, so turning it into an error usually produces an *unraisable* exception routed through `sys.unraisablehook` and printed, rather than a normal traceback propagating out of your function. It is loud, but it is not always a clean test failure. * `-X tracemalloc=25` (or `PYTHONTRACEMALLOC=25`) is the one that makes the warning *actionable*. With `tracemalloc` running, the warning text is followed by the allocation traceback of the object — that is, the exact line that called `open()` or created the socket. Without it, the warning tells you what leaked but not where it came from, which on a large codebase is barely half an answer. ### Where to switch it on The natural place is the test run. The standard-library `unittest` runner already enables the `default` warning filter for tests, so `ResourceWarning` surfaces there unless something explicitly silences it. For an application, add `-X dev -X tracemalloc=25` to the command line used in development and in CI, and leave production alone — dev mode's extra allocator checking is not something you want in a hot service, and the warning machinery is noise on a healthy system. Programmatic control is also available: `warnings.simplefilter("error", ResourceWarning)` inside a `warnings.catch_warnings()` block scopes the strictness to one test, and `record=True` collects the warnings so a test can assert on them directly. That pattern is how you write a regression test for "this helper must not leak a descriptor" rather than hoping someone reads the output. ### What the warning is and is not `ResourceWarning` is emitted at *deallocation* time, which means two things. First, an object that is still referenced has not warned yet — a leak that grows forever produces no warnings at all until the objects are dropped, so a clean run is not proof of correctness. Second, deallocation may happen at interpreter shutdown, when the warning machinery is partly torn down and output can be truncated or reordered. If you need certainty about a long-lived process, count descriptors directly rather than waiting for warnings. The complementary habit is to make the warning impossible in the first place: open every descriptor in a `with` block, or register it on a `contextlib.ExitStack`, so the close does not depend on the collector at all. Development mode is how you find the places where that discipline slipped, not a substitute for it.
- Why does adding tracemalloc change the value of the warning so much?Without it the warning says *what* leaked - `unclosed file <_io.TextIOWrapper name='...'>` - but not where it came from, and in a large codebase dozens of call sites open the same path. With `-X tracemalloc=25` the warning is followed by the object's allocation traceback, naming the line that called `open()` or created the socket. That turns a symptom into a patch.
- A whole test run passes under dev mode with no ResourceWarning. Does that prove nothing leaks?No. The warning fires at deallocation, so an object that is still referenced has not warned yet. A leak held alive by a cache, a module-level registry or a long-lived worker produces no warning at all while it grows. For a long-running process the reliable check is counting open descriptors over time, not waiting for finalizers.
- Would you run production with -X dev?No. Development mode adds allocator debug checks and turns on warning machinery that is pure noise on a healthy service; it is designed for development and CI. Enable it in the test command and in local runs, and keep production lean, with a descriptor-count metric as the production-side signal instead.
saying these in an interview costs you the question
- Thinks ResourceWarning is printed by default
- Confuses -X dev with -O or a debug interpreter build
- Believes dev mode changes program behaviour, not just reporting
- Assumes no warnings means no leaks
- Recommends running production under development mode