From dc2e9044c3b2576e68a4597af53e101df5f53cce Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Wed, 14 Oct 2026 07:54:28 -0700 Subject: [PATCH 1/4] 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 247efc01527bb14..54145bffbe3c7e5 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 8bc8e507683cb50..be3d4242b1e0580 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/4] 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 000000000000000..bcc37718ea76b2a --- /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/4] 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 be3d4242b1e0580..fa107e07534e470 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): From fbc25a0195af22bce94584967acc773bfd56fbb3 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Thu, 15 Oct 2026 15:02:11 -0700 Subject: [PATCH 4/4] Import warnings eagerly in gzip and test closing at shutdown --- Lib/gzip.py | 14 +++++------ Lib/test/test_gzip.py | 24 +++++++++---------- ...-10-14-15-50-59.gh-issue-159422.16mSqQ.rst | 7 +++--- 3 files changed, 21 insertions(+), 24 deletions(-) diff --git a/Lib/gzip.py b/Lib/gzip.py index 54145bffbe3c7e5..88d9157fea3209e 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -11,6 +11,7 @@ import struct import sys import time +import warnings # must stay eager to support finalization import weakref import zlib from compression._common import _streams @@ -229,7 +230,6 @@ def __init__(self, filename=None, mode=None, elif mode.startswith(('w', 'a', 'x')): if origmode is None: - import warnings warnings.warn( "GzipFile was opened for writing, but this will " "change in future Python releases. " @@ -467,13 +467,11 @@ def readline(self, size=-1): return self._buffer.readline(size) def __del__(self): - try: - if self.mode == WRITE and not self.closed: - import warnings - warnings.warn("unclosed GzipFile", - ResourceWarning, source=self, stacklevel=2) - finally: - super().__del__() + if self.mode == WRITE and not self.closed: + warnings.warn("unclosed GzipFile", + ResourceWarning, source=self, stacklevel=2) + + 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 fa107e07534e470..5473f18b57fc1c9 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, gc_collect +from test.support import catch_unraisable_exception 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,18 +965,16 @@ def test_refloop_unraisable(self): gc.collect() self.assertIsNone(cm.unraisable) - 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) + @requires_subprocess() + def test_unclosed_at_shutdown(self): + rc, out, err = assert_python_ok("-c", f"""if 1: + import gzip + f = gzip.GzipFile({self.filename!r}, "wb") + f.write({data1!r}) + """) + self.assertNotIn(b"Exception ignored", err) + with gzip.GzipFile(self.filename) as f: + self.assertEqual(f.read(), data1) class TestOpen(BaseTest): 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 index bcc37718ea76b2a..9250b8f0036c3f8 100644 --- 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 @@ -1,3 +1,4 @@ -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. +:mod:`gzip` now imports :mod:`warnings` eagerly so that a writable +:class:`~gzip.GzipFile` left unclosed at interpreter shutdown can emit its +:exc:`ResourceWarning` and be closed instead of failing with an +:exc:`ImportError`.