https://github.com/python/cpython/commit/874218f7d6ef53228f2f291cb8896082306916f9
commit: 874218f7d6ef53228f2f291cb8896082306916f9
branch: 3.14
author: Miss Islington (bot) <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-10-02T14:25:31Z
summary:

[3.14] gh-156321: Don't eagerly log exceptions from a cancelled 
asyncio.shield() (GH-158590) (#158599)

gh-156321: Don't eagerly log exceptions from a cancelled asyncio.shield() 
(GH-158590)

When the outer future returned by shield() was cancelled, a callback was
added to the inner future that reported its exception through the loop
exception handler as soon as it completed, even if the exception was
retrieved afterwards by awaiting the inner future or calling
`exception()`. Remove the callback; an exception nobody retrieves is
still reported by the inner future itself when it is garbage collected,
like any other future.
(cherry picked from commit 2095eab5f5005da2e773bd909c83e2a900b00ae2)

Co-authored-by: Kumar Aditya <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-10-01-16-45-00.gh-issue-156321.Kq3xVt.rst
M Lib/asyncio/tasks.py
M Lib/test/test_asyncio/test_tasks.py

diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py
index bd6a03f530afd2e..7819286577d895e 100644
--- a/Lib/asyncio/tasks.py
+++ b/Lib/asyncio/tasks.py
@@ -927,25 +927,6 @@ def _done_callback(fut, cur_task=cur_task):
     return outer
 
 
-def _log_on_exception(fut):
-    if fut.cancelled():
-        return
-
-    exc = fut.exception()
-    if exc is None:
-        return
-
-    context = {
-        'message':
-        f'{exc.__class__.__name__} exception in shielded future',
-        'exception': exc,
-        'future': fut,
-    }
-    if fut._source_traceback:
-        context['source_traceback'] = fut._source_traceback
-    fut._loop.call_exception_handler(context)
-
-
 def shield(arg):
     """Wait for a future, shielding it from cancellation.
 
@@ -1010,9 +991,6 @@ def _inner_done_callback(inner):
     def _outer_done_callback(outer):
         if not inner.done():
             inner.remove_done_callback(_inner_done_callback)
-            # Keep only one callback to log on cancel
-            inner.remove_done_callback(_log_on_exception)
-            inner.add_done_callback(_log_on_exception)
             if cur_task is not None:
                 inner.remove_done_callback(_clear_awaited_by_callback)
                 futures.future_discard_from_awaited_by(inner, cur_task)
diff --git a/Lib/test/test_asyncio/test_tasks.py 
b/Lib/test/test_asyncio/test_tasks.py
index 2eeedb1dee6b36b..a44232337080ea4 100644
--- a/Lib/test/test_asyncio/test_tasks.py
+++ b/Lib/test/test_asyncio/test_tasks.py
@@ -2148,7 +2148,7 @@ def test_shield_cancel_outer(self):
         self.assertTrue(outer.cancelled())
         self.assertEqual(0, 0 if outer._callbacks is None else 
len(outer._callbacks))
         self.assertFalse(inner._asyncio_awaited_by)
-        self.assertTrue({f for f, _ctx in inner._callbacks or []} <= 
{asyncio.tasks._log_on_exception})
+        self.assertFalse(inner._callbacks)
 
     def test_shield_cancel_outer_result(self):
         mock_handler = mock.Mock()
@@ -2163,6 +2163,8 @@ def test_shield_cancel_outer_result(self):
         mock_handler.assert_not_called()
 
     def test_shield_cancel_outer_exception(self):
+        # gh-156321: an exception in the inner future must not be reported
+        # eagerly, as it may still be retrieved later.
         mock_handler = mock.Mock()
         self.loop.set_exception_handler(mock_handler)
         inner = self.new_future(self.loop)
@@ -2172,7 +2174,29 @@ def test_shield_cancel_outer_exception(self):
         test_utils.run_briefly(self.loop)
         inner.set_exception(Exception('foo'))
         test_utils.run_briefly(self.loop)
+        mock_handler.assert_not_called()
+        self.assertIsInstance(inner.exception(), Exception)
+
+    def test_shield_cancel_outer_exception_never_retrieved(self):
+        # gh-156321: an exception nobody retrieves is reported by the inner
+        # future itself when it is garbage collected, like any other future.
+        mock_handler = mock.Mock()
+        self.loop.set_exception_handler(mock_handler)
+        inner = self.new_future(self.loop)
+        outer = asyncio.shield(inner)
+        test_utils.run_briefly(self.loop)
+        outer.cancel()
+        test_utils.run_briefly(self.loop)
+        inner.set_exception(Exception('foo'))
+        test_utils.run_briefly(self.loop)
+        mock_handler.assert_not_called()
+        inner = None
+        outer = None
+        support.gc_collect()
         mock_handler.assert_called_once()
+        context = mock_handler.call_args[0][1]
+        self.assertEndsWith(context['message'], 'exception was never 
retrieved')
+        self.assertIsInstance(context['exception'], Exception)
 
     def test_shield_cancel_outer_in_task(self):
         inner = self.new_future(self.loop)
@@ -2187,9 +2211,9 @@ async def coro():
         task = self.new_task(self.loop, coro())
         self.loop.run_until_complete(task)
         self.assertFalse(inner._asyncio_awaited_by)
-        self.assertTrue({f for f, _ctx in inner._callbacks or []} <= 
{asyncio.tasks._log_on_exception})
+        self.assertFalse(inner._callbacks)
 
-    def test_shield_duplicate_log_once(self):
+    def test_shield_cancel_outer_twice_exception(self):
         mock_handler = mock.Mock()
         self.loop.set_exception_handler(mock_handler)
         inner = self.new_future(self.loop)
@@ -2201,9 +2225,11 @@ def test_shield_duplicate_log_once(self):
         test_utils.run_briefly(self.loop)
         outer.cancel()
         test_utils.run_briefly(self.loop)
+        self.assertFalse(inner._callbacks)
         inner.set_exception(Exception('foo'))
         test_utils.run_briefly(self.loop)
-        mock_handler.assert_called_once()
+        mock_handler.assert_not_called()
+        self.assertIsInstance(inner.exception(), Exception)
 
     def test_shield_shortcut(self):
         fut = self.new_future(self.loop)
diff --git 
a/Misc/NEWS.d/next/Library/2026-10-01-16-45-00.gh-issue-156321.Kq3xVt.rst 
b/Misc/NEWS.d/next/Library/2026-10-01-16-45-00.gh-issue-156321.Kq3xVt.rst
new file mode 100644
index 000000000000000..2101e9a899e5073
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-10-01-16-45-00.gh-issue-156321.Kq3xVt.rst
@@ -0,0 +1,5 @@
+Fix :func:`asyncio.shield` reporting an exception from the inner future
+through the loop exception handler as soon as it completes after the shield
+was cancelled, even when the exception is retrieved afterwards. The
+exception is now reported only if it is never retrieved, as with any other
+future.

_______________________________________________
Python-checkins mailing list -- [email protected]
To unsubscribe send an email to [email protected]
https://mail.python.org/mailman3//lists/python-checkins.python.org
Member address: [email protected]

Reply via email to