diff --git a/Lib/test/test_zipfile/test_core.py b/Lib/test/test_zipfile/test_core.py index 708db4d4387df5e..10b3aad25fb9b41 100644 --- a/Lib/test/test_zipfile/test_core.py +++ b/Lib/test/test_zipfile/test_core.py @@ -26,7 +26,7 @@ from test.support import ( findfile, requires_zlib, requires_bz2, requires_lzma, requires_zstd, captured_stdout, captured_stderr, requires_subprocess, - cpython_only, gc_collect + cpython_only, gc_collect, catch_unraisable_exception ) from test.support.os_helper import ( TESTFN, unlink, rmtree, temp_dir, temp_cwd, fd_count, FakePath, @@ -4095,6 +4095,29 @@ def test_garbage_collection(self): zf.close() del zf + def test_garbage_collection_closes_when_warning_fails(self): + buf = io.BytesIO() + zf = zipfile.ZipFile(buf, "w") + zf.writestr("f.txt", b"data") + with warnings.catch_warnings(), catch_unraisable_exception() as cm: + warnings.simplefilter("error", ResourceWarning) + del zf + gc_collect() + self.assertIsInstance(cm.unraisable.exc_value, ResourceWarning) + with zipfile.ZipFile(buf) as zf: + self.assertEqual(zf.namelist(), ["f.txt"]) + + @requires_subprocess() + def test_unclosed_at_shutdown(self): + self.addCleanup(unlink, TESTFN) + script_helper.assert_python_ok("-c", f"""if 1: + import zipfile + zf = zipfile.ZipFile({TESTFN!r}, "w") + zf.writestr("f.txt", b"data") + """) + with zipfile.ZipFile(TESTFN) as zf: + self.assertEqual(zf.namelist(), ["f.txt"]) + def test_unsupported_version(self): # File has an extract_version of 120 data = (b'PK\x03\x04x\x00\x00\x00\x00\x00!p\xa1@\x00\x00\x00\x00\x00\x00' diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index d817bdd8769e7d7..70346cf2bffc8cc 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -2650,10 +2650,14 @@ def __del__(self): """Call the "close()" method in case the user forgot.""" # gh-81954: Warn if writable ZipFile is implicitly closed. # GC cleanup order is non-deterministic and can result in data loss. - if self.fp is not None and self.mode in ('w', 'x', 'a'): - warnings.warn(f"unclosed ZipFile {self!r}", - ResourceWarning, source=self, stacklevel=2) - self.close() + try: + # Wrap within a try to ensure the file is closed even if the + # warnings module is unable to load during finalization. + if self.fp is not None and self.mode in ('w', 'x', 'a'): + warnings.warn(f"unclosed ZipFile {self!r}", + ResourceWarning, source=self, stacklevel=2) + finally: + self.close() def close(self): """Close the file, and for mode 'w', 'x' and 'a' write the ending diff --git a/Misc/NEWS.d/next/Library/2026-10-14-14-47-05.gh-issue-81954.KvM0gY.rst b/Misc/NEWS.d/next/Library/2026-10-14-14-47-05.gh-issue-81954.KvM0gY.rst new file mode 100644 index 000000000000000..fddb9eabe96c2a1 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-14-14-47-05.gh-issue-81954.KvM0gY.rst @@ -0,0 +1,3 @@ +Fix data loss when a writable :class:`zipfile.ZipFile` that was never closed +is finalized during interpreter shutdown: :meth:`~zipfile.ZipFile.close` is +now called even if emitting the :exc:`ResourceWarning` fails.