https://github.com/python/cpython/commit/f76c43066fadbda6a87e94a0774934f30f762f2f
commit: f76c43066fadbda6a87e94a0774934f30f762f2f
branch: 3.15
author: Miss Islington (bot) <[email protected]>
committer: hugovk <[email protected]>
date: 2026-09-28T18:59:28+03:00
summary:

[3.15] gh-157301: Fix asyncio event loop hanging on a failed eager task start 
(GH-157302) (#157785)

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

files:
A Misc/NEWS.d/next/Library/2026-09-11-14-14-51.gh-issue-157301.QxcE2r.rst
M Lib/asyncio/tasks.py
M Lib/test/test_asyncio/test_tasks.py
M Modules/_asynciomodule.c

diff --git a/Lib/asyncio/tasks.py b/Lib/asyncio/tasks.py
index bd6a03f530afd2e..bdc577319c2ecc2 100644
--- a/Lib/asyncio/tasks.py
+++ b/Lib/asyncio/tasks.py
@@ -103,11 +103,20 @@ def __init__(self, coro, *, loop=None, name=None, 
context=None,
         self._coro = coro
         if context is None:
             self._context = contextvars.copy_context()
+        elif not isinstance(context, contextvars.Context):
+            # gh-157301: the passed value must be a contextvars.Context
+            self._log_destroy_pending = False
+            raise TypeError('a contextvars.Context was expected, '
+                            f'got {type(context).__name__}')
         else:
             self._context = context
 
         if eager_start and self._loop.is_running():
-            self.__eager_start()
+            try:
+                self.__eager_start()
+            except:
+                self._log_destroy_pending = False
+                raise
         else:
             self._loop.call_soon(self.__step, context=self._context)
             _py_register_task(self)
diff --git a/Lib/test/test_asyncio/test_tasks.py 
b/Lib/test/test_asyncio/test_tasks.py
index 3cb4679e8955bfa..a2f80edeb1d739b 100644
--- a/Lib/test/test_asyncio/test_tasks.py
+++ b/Lib/test/test_asyncio/test_tasks.py
@@ -2630,6 +2630,68 @@ async def main():
         finally:
             loop.close()
 
+    def test_context_not_a_context(self):
+        # gh-157301
+        async def coro():
+            pass
+
+        loop = asyncio.new_event_loop()
+        c = coro()
+        try:
+            with self.assertRaises(TypeError):
+                self.new_task(loop, c, context='not a context')
+        finally:
+            c.close()
+            loop.close()
+
+    def test_context_not_a_context_leaves_loop_usable(self):
+        # gh-157301
+        async def coro():
+            pass
+
+        async def main():
+            c = coro()
+            try:
+                with self.assertRaises(TypeError):
+                    self.new_task(loop, c, context='not a context',
+                                  eager_start=True)
+            finally:
+                c.close()
+            await asyncio.sleep(0)
+
+        loop = asyncio.new_event_loop()
+        loop.call_later(support.SHORT_TIMEOUT, loop.stop)
+        try:
+            loop.run_until_complete(self.new_task(loop, main()))
+        finally:
+            loop.close()
+
+    def test_context_already_entered_leaves_loop_usable(self):
+        # gh-157301
+        async def coro():
+            pass
+
+        async def main():
+            ctx = contextvars.copy_context()
+
+            def inside():
+                c = coro()
+                try:
+                    with self.assertRaises(RuntimeError):
+                        self.new_task(loop, c, context=ctx, eager_start=True)
+                finally:
+                    c.close()
+
+            ctx.run(inside)
+            await asyncio.sleep(0)
+
+        loop = asyncio.new_event_loop()
+        loop.call_later(support.SHORT_TIMEOUT, loop.stop)
+        try:
+            loop.run_until_complete(self.new_task(loop, main()))
+        finally:
+            loop.close()
+
     def test_context_2(self):
         cvar = contextvars.ContextVar('cvar', default='nope')
 
diff --git 
a/Misc/NEWS.d/next/Library/2026-09-11-14-14-51.gh-issue-157301.QxcE2r.rst 
b/Misc/NEWS.d/next/Library/2026-09-11-14-14-51.gh-issue-157301.QxcE2r.rst
new file mode 100644
index 000000000000000..9a1d2e435be9066
--- /dev/null
+++ b/Misc/NEWS.d/next/Library/2026-09-11-14-14-51.gh-issue-157301.QxcE2r.rst
@@ -0,0 +1,2 @@
+Fix :class:`asyncio.Task` hanging the event loop when an eager start fails
+to enter the task's context.
diff --git a/Modules/_asynciomodule.c b/Modules/_asynciomodule.c
index a27190dadcd48c0..ff7991827ee9635 100644
--- a/Modules/_asynciomodule.c
+++ b/Modules/_asynciomodule.c
@@ -2333,6 +2333,13 @@ _asyncio_Task___init___impl(TaskObj *self, PyObject 
*coro, PyObject *loop,
         if (self->task_context == NULL) {
             return -1;
         }
+    } else if (!PyContext_CheckExact(context)) {
+        // gh-157301: the passed value must be a contextvars.Context
+        self->task_log_destroy_pending = 0;
+        PyErr_Format(PyExc_TypeError,
+                     "a contextvars.Context was expected, got %T",
+                     context);
+        return -1;
     } else {
         Py_XSETREF(self->task_context, Py_NewRef(context));
     }
@@ -3478,7 +3485,13 @@ task_eager_start(_PyThreadStateImpl *ts, asyncio_state 
*state, TaskObj *task)
     // it will continue as a regular (non-eager) asyncio task
     register_task(ts, task);
 
+    assert(PyContext_CheckExact(task->task_context));
     if (_PyContext_Enter(&ts->base, task->task_context) == -1) {
+        // gh-157301: a failed enter must not leave the task current and 
registered
+        task->task_log_destroy_pending = 0;
+        PyObject *curtask = swap_current_task(ts, task->task_loop, prevtask);
+        Py_XDECREF(curtask);
+        unregister_task(task);
         Py_DECREF(prevtask);
         return -1;
     }

_______________________________________________
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