RustPython/RustPython · warning · ResourceWarning

unclosed file {repr}

Error message

unclosed file {repr}

What it means

ResourceWarning emitted from FileIO's dealloc hook when the object is dropped while its fd is still >= 0 and closefd is true — i.e. the file was never close()d and cleanup fell to the garbage collector. File descriptors are a finite process-global resource, so both CPython and RustPython warn at destruction to make the leak visible. If raising the warning itself fails, it is routed through run_unraisable.

Source

Thrown at crates/vm/src/stdlib/_io.rs:6064

        fn __getstate__(zelf: PyObjectRef, vm: &VirtualMachine) -> PyResult {
            Err(vm.new_type_error(format!("cannot pickle '{}' instances", zelf.class().name())))
        }

        /// fileio_dealloc_warn in Modules/_io/fileio.c
        #[pymethod(name = "_dealloc_warn")]
        fn _dealloc_warn_method(zelf: &Py<Self>, source: PyObjectRef, vm: &VirtualMachine) {
            Self::dealloc_warn(zelf, source, vm);
        }
    }

    impl FileIO {
        /// Issue ResourceWarning if fd is still open and closefd is true.
        fn dealloc_warn(zelf: &Py<Self>, source: PyObjectRef, vm: &VirtualMachine) {
            if zelf.fd.load() >= 0 && zelf.closefd.load() {
                let repr = source
                    .repr(vm)
                    .map_or_else(|_| Wtf8Buf::from("<file>"), |s| s.as_wtf8().to_owned());
                if let Err(e) = crate::stdlib::_warnings::warn(
                    vm.ctx.exceptions.resource_warning,
                    format!("unclosed file {repr}"),
                    1,
                    vm,
                ) {
                    vm.run_unraisable(e, None, zelf.as_object().to_owned());
                }
            }
        }
    }

    impl Destructor for FileIO {
        fn slot_del(zelf: &PyObject, vm: &VirtualMachine) -> PyResult<()> {
            if let Some(fileio) = zelf.downcast_ref::<Self>() {
                fileio.finalizing.store(true);
            }
            iobase_finalize(zelf, vm);
            Ok(())

View on GitHub (pinned to 5dc83d997e)

Solutions

  1. Use a with statement: with open(p) as f: ...
  2. Use try/finally with close() when a with block does not fit the control flow
  3. Give every cached file object an owner responsible for closing it
  4. Run tests with python -X dev or -W error::ResourceWarning so leaks fail fast

Example fix

# before
f = open("log.txt", "rb")
data = f.read()

# after
with open("log.txt", "rb") as f:
    data = f.read()
Defensive patterns

Strategy: try-catch

Validate before calling

import gc, warnings

def assert_no_unclosed_files(stage):
    gc.collect()
    with warnings.catch_warnings(record=True) as caught:
        warnings.simplefilter("always", ResourceWarning)
        gc.collect()
    leaks = [str(w.message) for w in caught if "unclosed file" in str(w.message)]
    assert not leaks, f"{stage}: {leaks}"

Try / catch

import warnings
with warnings.catch_warnings():
    warnings.simplefilter("error", ResourceWarning)
    code_under_test()  # raises ResourceWarning as an exception at GC time

Prevention

When it happens

Trigger: open()/io.FileIO without close(); exceptions that skip the close() call; storing file objects in caches/containers and dropping them later; reference cycles that delay collection. Usually observed only when GC actually runs, or under -X dev / -Wd.

Common situations: Long-running servers gradually exhausting fds; scripts that open many files in a loop; test suites that fail intermittently with 'Too many open files' after warnings are enabled.

Related errors


AI-assisted analysis of RustPython/RustPython@5dc83d997e (2026-08-17). Data as JSON: /api/errors/b7a49a5b7b83734b. Report an issue: GitHub.