From dc2e9044c3b2576e68a4597af53e101df5f53cce Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Wed, 14 Oct 2026 07:54:28 -0700 Subject: [PATCH 1/3] Always close GzipFile in __del__ even if the warning fails GzipFile.__del__ emits a ResourceWarning before closing the file. If emitting it raises, close() is skipped and the gzip trailer is never written, truncating the output. This happens at interpreter shutdown, when the function-level `import warnings` fails, and with -W error::ResourceWarning. Close the file in a finally block. --- Lib/gzip.py | 13 +++++++------ Lib/test/test_gzip.py | 11 +++++++++++ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/Lib/gzip.py b/Lib/gzip.py index 247efc01527bb1..54145bffbe3c7e 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -467,12 +467,13 @@ def readline(self, size=-1): return self._buffer.readline(size) def __del__(self): - if self.mode == WRITE and not self.closed: - import warnings - warnings.warn("unclosed GzipFile", - ResourceWarning, source=self, stacklevel=2) - - super().__del__() + try: + if self.mode == WRITE and not self.closed: + import warnings + warnings.warn("unclosed GzipFile", + ResourceWarning, source=self, stacklevel=2) + finally: + super().__del__() def _read_exact(fp, n): '''Read exactly *n* bytes from `fp` diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index 8bc8e507683cb5..be3d4242b1e058 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -965,6 +965,17 @@ def test_refloop_unraisable(self): gc.collect() self.assertIsNone(cm.unraisable) + @requires_subprocess() + def test_unclosed_at_shutdown(self): + # The file must still be closed if the warning fails at shutdown. + assert_python_ok("-c", f"""if 1: + import gzip + f = gzip.open({self.filename!r}, "wb") + f.write({data1!r}) + """) + with gzip.open(self.filename, "rb") as f: + self.assertEqual(f.read(), data1) + class TestOpen(BaseTest): def test_binary_modes(self): From 49e93807d2004691f0e389be5f7274f6f471ef08 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Wed, 14 Oct 2026 08:53:22 -0700 Subject: [PATCH 2/3] Add NEWS entry --- .../Library/2026-10-14-15-50-59.gh-issue-159422.16mSqQ.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-14-15-50-59.gh-issue-159422.16mSqQ.rst diff --git a/Misc/NEWS.d/next/Library/2026-10-14-15-50-59.gh-issue-159422.16mSqQ.rst b/Misc/NEWS.d/next/Library/2026-10-14-15-50-59.gh-issue-159422.16mSqQ.rst new file mode 100644 index 00000000000000..bcc37718ea76b2 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-14-15-50-59.gh-issue-159422.16mSqQ.rst @@ -0,0 +1,3 @@ +Fix truncated output when a writable :class:`gzip.GzipFile` that was never +closed is finalized and emitting its :exc:`ResourceWarning` fails, for +example at interpreter shutdown. The file is now always closed. From 8ef5db901cd0b376ba6867c6c0cb3090fad59dc2 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Wed, 14 Oct 2026 14:45:17 -0700 Subject: [PATCH 3/3] Test GzipFile.__del__ by mocking warnings.warn instead of at shutdown The order of __del__ calls at interpreter shutdown is not deterministic on the free-threaded build, so make the warning raise directly. --- Lib/test/test_gzip.py | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/Lib/test/test_gzip.py b/Lib/test/test_gzip.py index be3d4242b1e058..fa107e07534e47 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -11,7 +11,7 @@ import unittest from subprocess import PIPE, Popen from unittest import mock -from test.support import catch_unraisable_exception +from test.support import catch_unraisable_exception, gc_collect from test.support import force_not_colorized_test_class, import_helper from test.support import os_helper from test.support import _4G, bigmemtest, requires_subprocess @@ -965,16 +965,18 @@ def test_refloop_unraisable(self): gc.collect() self.assertIsNone(cm.unraisable) - @requires_subprocess() - def test_unclosed_at_shutdown(self): - # The file must still be closed if the warning fails at shutdown. - assert_python_ok("-c", f"""if 1: - import gzip - f = gzip.open({self.filename!r}, "wb") - f.write({data1!r}) - """) - with gzip.open(self.filename, "rb") as f: - self.assertEqual(f.read(), data1) + def test_del_closes_if_warning_fails(self): + # The file must still be closed if emitting the ResourceWarning + # raises, as can happen at interpreter shutdown. + b = io.BytesIO() + f = gzip.GzipFile(fileobj=b, mode="wb") + f.write(data1) + with (mock.patch("warnings.warn", side_effect=RuntimeError), + catch_unraisable_exception() as cm): + del f + gc_collect() + self.assertIs(cm.unraisable.exc_type, RuntimeError) + self.assertEqual(gzip.decompress(b.getvalue()), data1) class TestOpen(BaseTest):