https://github.com/python/cpython/commit/a8e1d25355004ba8790e34dbf7fb5268674e7f08
commit: a8e1d25355004ba8790e34dbf7fb5268674e7f08
branch: main
author: tonghuaroot (童话) <[email protected]>
committer: serhiy-storchaka <[email protected]>
date: 2026-08-17T15:44:40+03:00
summary:

gh-155852: Do not cancel remaining Executor.map calls on a callable's 
TimeoutError (GH-155853)

A TimeoutError raised by the mapped callable was re-raised like the
map(timeout=...) wait timeout, aborting the iteration and cancelling the
remaining calls, unlike every other exception since gh-108518. A wait timeout
only occurs while the future is still running, so a TimeoutError from an
already-finished future is treated as the callable's own result.

files:
M Lib/concurrent/futures/_base.py
M Lib/test/test_concurrent_futures/executor.py

diff --git a/Lib/concurrent/futures/_base.py b/Lib/concurrent/futures/_base.py
index cc335d9aa1ea55d..e728b8e0a91f744 100644
--- a/Lib/concurrent/futures/_base.py
+++ b/Lib/concurrent/futures/_base.py
@@ -309,10 +309,13 @@ def wait(fs, timeout=None, return_when=ALL_COMPLETED):
 def _result_or_cancel(fut, timeout=None):
     try:
         try:
-            return (fut.result(timeout), None)
-        except TimeoutError:
-            raise
-        except BaseException as exc:
+            # fut.exception() returns the call's own error but raises
+            # TimeoutError only for a map() timeout.
+            exc = fut.exception(timeout)
+            if exc is not None:
+                return (None, exc)
+            return (fut.result(), None)
+        except CancelledError as exc:
             return (None, exc)
         finally:
             fut.cancel()
diff --git a/Lib/test/test_concurrent_futures/executor.py 
b/Lib/test/test_concurrent_futures/executor.py
index 5d9f27c83bf9a81..ff7bd0db0c2199c 100644
--- a/Lib/test/test_concurrent_futures/executor.py
+++ b/Lib/test/test_concurrent_futures/executor.py
@@ -29,6 +29,12 @@ def raiser(exception, msg='std'):
     raise exception(msg)
 
 
+def timeout_on_one(x):
+    if x == 1:
+        raise TimeoutError
+    return x
+
+
 class FalseyBoolException(Exception):
     def __bool__(self):
         return False
@@ -87,6 +93,20 @@ def test_map_exception(self):
         self.assertRaises(StopIteration, next, i)
         self.assertRaises(StopIteration, next, i)
 
+    @warnings_helper.ignore_fork_in_thread_deprecation_warnings()
+    def test_map_timeout_from_callable(self):
+        # A TimeoutError from the callable is not the map() timeout, whether
+        # or not a map() timeout is set.
+        for timeout in (None, support.SHORT_TIMEOUT):
+            with self.subTest(timeout=timeout):
+                i = self.executor.map(timeout_on_one, [0, 1, 2, 3],
+                                      timeout=timeout)
+                self.assertEqual(next(i), 0)
+                self.assertRaises(TimeoutError, next, i)
+                self.assertEqual(next(i), 2)
+                self.assertEqual(next(i), 3)
+                self.assertRaises(StopIteration, next, i)
+
     @warnings_helper.ignore_fork_in_thread_deprecation_warnings()
     @support.requires_resource('walltime')
     def test_map_timeout(self):

_______________________________________________
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