From e48aea12ac001e2e33875f93a43c6fe465a05d2b Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Sun, 4 Oct 2026 19:28:56 -0700 Subject: [PATCH 1/3] Always close ZipFile in __del__ even if the warning fails ZipFile.__del__ emitted its ResourceWarning before calling close(). When warnings.warn() raised, close() was skipped and the central directory of a writable archive was never written, leaving a corrupt file. This happens at interpreter shutdown, where resolving the lazy "warnings" import raises ImportError, and with -W error::ResourceWarning at any time. Call close() in a finally block. --- Lib/test/test_zipfile/test_core.py | 25 ++++++++++++++++++++++++- Lib/zipfile/__init__.py | 10 ++++++---- 2 files changed, 30 insertions(+), 5 deletions(-) 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..debfbaabe34a009 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -2650,10 +2650,12 @@ 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: + 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 From 29ef75407d7691e0d729913c5172924c6f440547 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Wed, 14 Oct 2026 07:48:14 -0700 Subject: [PATCH 2/3] Add NEWS entry --- .../next/Library/2026-10-14-14-47-05.gh-issue-81954.KvM0gY.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-14-14-47-05.gh-issue-81954.KvM0gY.rst 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. From 9c41b9519cf7bda2b6e81c7863e9dc725f27bf12 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Wed, 14 Oct 2026 14:42:47 -0700 Subject: [PATCH 3/3] Explain why ZipFile.__del__ wraps the warning in try/finally --- Lib/zipfile/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Lib/zipfile/__init__.py b/Lib/zipfile/__init__.py index debfbaabe34a009..70346cf2bffc8cc 100644 --- a/Lib/zipfile/__init__.py +++ b/Lib/zipfile/__init__.py @@ -2651,6 +2651,8 @@ def __del__(self): # gh-81954: Warn if writable ZipFile is implicitly closed. # GC cleanup order is non-deterministic and can result in data loss. 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)