https://github.com/python/cpython/commit/c87f05bdd1bd6bb69ebab4a2d050ff76134724bf
commit: c87f05bdd1bd6bb69ebab4a2d050ff76134724bf
branch: 3.14
author: An Long <[email protected]>
committer: kumaraditya303 <[email protected]>
date: 2026-08-30T14:05:06+05:30
summary:

[3.14] gh-156408: Fix asyncio.print_call_graph() on a finished task (GH-156410) 
(#156564)

(cherry picked from commit 6c425f1d18d3915cb32e3372647db24bad7ab92d)

Co-authored-by: Timofei Ivankov <[email protected]>

files:
A Misc/NEWS.d/next/Library/2026-08-26-12-48-10.gh-issue-156408.-Yd0k7.rst
M Lib/asyncio/graph.py
M Lib/test/test_asyncio/test_graph.py

diff --git a/Lib/asyncio/graph.py b/Lib/asyncio/graph.py
index 35f7fa62140bd49..54df7afa5553b97 100644
--- a/Lib/asyncio/graph.py
+++ b/Lib/asyncio/graph.py
@@ -58,7 +58,8 @@ def _build_graph_for_future(
     while coro is not None:
         if hasattr(coro, 'cr_await'):
             # A native coroutine or duck-type compatible iterator
-            st.append(FrameCallGraphEntry(coro.cr_frame))
+            if coro.cr_frame is not None:
+                st.append(FrameCallGraphEntry(coro.cr_frame))
             coro = coro.cr_await
         elif hasattr(coro, 'ag_await'):
             # A native async generator or duck-type compatible iterator
diff --git a/Lib/test/test_asyncio/test_graph.py 
b/Lib/test/test_asyncio/test_graph.py
index 2f22fbccba42bc8..46a7c3d3192837d 100644
--- a/Lib/test/test_asyncio/test_graph.py
+++ b/Lib/test/test_asyncio/test_graph.py
@@ -345,6 +345,24 @@ async def main():
 
         self.assertTrue(stack_for_fut[1].startswith('* Future(id='))
 
+    async def test_call_graph_finished_task(self):
+        # gh-156408: the call graph must not record a finished coroutine's 
None frame
+        async def boom():
+            raise ValueError
+
+        done = asyncio.create_task(asyncio.sleep(0), name='done')
+        failed = asyncio.create_task(boom(), name='failed')
+        cancelled = asyncio.create_task(asyncio.Event().wait(), 
name='cancelled')
+        cancelled.cancel()
+        await asyncio.gather(done, failed, cancelled, return_exceptions=True)
+
+        for task in (done, failed, cancelled):
+            with self.subTest(task=task.get_name()):
+                buf = io.StringIO()
+                asyncio.print_call_graph(task, file=buf)
+                self.assertEqual(asyncio.capture_call_graph(task).call_stack, 
())
+                self.assertIn(f"name={task.get_name()!r}", buf.getvalue())
+
 
 @unittest.skipIf(
     not hasattr(asyncio.futures, "_c_future_add_to_awaited_by"),
diff --git 
a/Misc/NEWS.d/next/Library/2026-08-26-12-48-10.gh-issue-156408.-Yd0k7.rst 
b/Misc/NEWS.d/next/Library/2026-08-26-12-48-10.gh-issue-156408.-Yd0k7.rst
new file mode 100644
index 000000000000000..62c92838b5c1266
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-08-26-12-48-10.gh-issue-156408.-Yd0k7.rst
@@ -0,0 +1,2 @@
+Fix :func:`asyncio.print_call_graph` raising :exc:`AttributeError` when
+called on a task that has already finished.

_______________________________________________
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