From dc2e9044c3b2576e68a4597af53e101df5f53cce Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Wed, 14 Oct 2026 07:54:28 -0700 Subject: [PATCH 1/5] 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/5] 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/5] 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): From fbc25a0195af22bce94584967acc773bfd56fbb3 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Thu, 15 Oct 2026 15:02:11 -0700 Subject: [PATCH 4/5] 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 54145bffbe3c7e..88d9157fea3209 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 fa107e07534e47..5473f18b57fc1c 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 bcc37718ea76b2..9250b8f0036c3f 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`. From 7b855208e683a63f4f25ef904600966a0d60dc68 Mon Sep 17 00:00:00 2001 From: Brittany Reynoso Date: Mon, 19 Oct 2026 08:51:15 -0700 Subject: [PATCH 5/5] Restore try/finally in GzipFile.__del__ using the zipfile pattern Bind warnings.warn as a default argument so it is still available at interpreter shutdown, and always close the file even if the warning raises. Replace the shutdown subprocess test, which depends on finalization order and fails on free-threaded builds, with one that escalates ResourceWarning to an error. --- Lib/gzip.py | 15 ++++++----- Lib/test/test_gzip.py | 25 +++++++++++-------- ...-10-14-15-50-59.gh-issue-159422.16mSqQ.rst | 6 ++--- 3 files changed, 25 insertions(+), 21 deletions(-) diff --git a/Lib/gzip.py b/Lib/gzip.py index 88d9157fea3209..ea184ab88f1eb0 100644 --- a/Lib/gzip.py +++ b/Lib/gzip.py @@ -466,12 +466,15 @@ def readline(self, size=-1): self._check_not_closed() return self._buffer.readline(size) - def __del__(self): - if self.mode == WRITE and not self.closed: - warnings.warn("unclosed GzipFile", - ResourceWarning, source=self, stacklevel=2) - - super().__del__() + def __del__(self, _warn=warnings.warn): + try: + # Wrap within a try to ensure the file is closed even if the + # warning raises, e.g. when ResourceWarning is made an error. + if self.mode == WRITE and not self.closed: + _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 5473f18b57fc1c..8115519cf6555a 100644 --- a/Lib/test/test_gzip.py +++ b/Lib/test/test_gzip.py @@ -9,9 +9,10 @@ import struct import sys import unittest +import warnings 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 +966,18 @@ def test_refloop_unraisable(self): gc.collect() self.assertIsNone(cm.unraisable) - @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) + def test_del_closes_if_warning_fails(self): + # ResourceWarning escalated to an error is reported as unraisable, + # and the file is still closed. + b = io.BytesIO() + f = gzip.GzipFile(fileobj=b, mode="wb") + f.write(data1) + with (warnings.catch_warnings(action="error", category=ResourceWarning), + catch_unraisable_exception() as cm): + del f + gc_collect() + self.assertIs(cm.unraisable.exc_type, ResourceWarning) + self.assertEqual(gzip.decompress(b.getvalue()), 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 9250b8f0036c3f..41e09f11942de9 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,4 +1,2 @@ -: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`. +Fix :class:`gzip.GzipFile` not being closed at interpreter shutdown when +emitting its :exc:`ResourceWarning` failed with an :exc:`ImportError`.