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

serhiy-storchaka <[email protected]>
Newsgroups gmane.comp.python.cvs
Message-ID <[email protected]>
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]
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.