From 5ce30ec209fdc8a31396f309bb3045d4771a86d8 Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Sat, 15 Aug 2026 22:14:18 +0800 Subject: [PATCH 1/2] gh-155852: Do not cancel remaining Executor.map calls on a callable's TimeoutError 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. --- Lib/concurrent/futures/_base.py | 6 +++++- Lib/test/test_concurrent_futures/executor.py | 19 +++++++++++++++++++ ...-08-15-16-00-00.gh-issue-155852.MapTmo.rst | 3 +++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 Misc/NEWS.d/next/Library/2026-08-15-16-00-00.gh-issue-155852.MapTmo.rst diff --git a/Lib/concurrent/futures/_base.py b/Lib/concurrent/futures/_base.py index cc335d9aa1ea55..4cf52f87bdd761 100644 --- a/Lib/concurrent/futures/_base.py +++ b/Lib/concurrent/futures/_base.py @@ -310,7 +310,11 @@ def _result_or_cancel(fut, timeout=None): try: try: return (fut.result(timeout), None) - except TimeoutError: + except TimeoutError as exc: + if fut.done(): + # The future already finished, so this is the callable's own + # TimeoutError, not the map() timeout waiting for the future. + return (None, exc) raise except BaseException as exc: return (None, exc) diff --git a/Lib/test/test_concurrent_futures/executor.py b/Lib/test/test_concurrent_futures/executor.py index 5d9f27c83bf9a8..e4f91f2067052f 100644 --- a/Lib/test/test_concurrent_futures/executor.py +++ b/Lib/test/test_concurrent_futures/executor.py @@ -29,6 +29,13 @@ def raiser(exception, msg='std'): raise exception(msg) +# Used in test_map_timeout_from_callable +def timeout_on_one(x): + if x == 1: + raise TimeoutError + return x + + class FalseyBoolException(Exception): def __bool__(self): return False @@ -87,6 +94,18 @@ 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 raised by the mapped callable must not be treated as a + # map() timeout: the remaining calls keep running, like any other + # exception. + i = self.executor.map(timeout_on_one, [0, 1, 2, 3]) + 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): diff --git a/Misc/NEWS.d/next/Library/2026-08-15-16-00-00.gh-issue-155852.MapTmo.rst b/Misc/NEWS.d/next/Library/2026-08-15-16-00-00.gh-issue-155852.MapTmo.rst new file mode 100644 index 00000000000000..57be15bad73923 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-08-15-16-00-00.gh-issue-155852.MapTmo.rst @@ -0,0 +1,3 @@ +Fix :meth:`concurrent.futures.Executor.map` cancelling the remaining calls when +the mapped callable raises :exc:`TimeoutError`; such an exception is now +propagated like any other, without stopping the iteration. From 355dc7cec73c6ec862da450e1b5b0b58162bb4de Mon Sep 17 00:00:00 2001 From: tonghuaroot Date: Sat, 15 Aug 2026 22:29:07 +0800 Subject: [PATCH 2/2] Trim the regression test comment --- Lib/test/test_concurrent_futures/executor.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/Lib/test/test_concurrent_futures/executor.py b/Lib/test/test_concurrent_futures/executor.py index e4f91f2067052f..1b1eac63945368 100644 --- a/Lib/test/test_concurrent_futures/executor.py +++ b/Lib/test/test_concurrent_futures/executor.py @@ -96,9 +96,7 @@ def test_map_exception(self): @warnings_helper.ignore_fork_in_thread_deprecation_warnings() def test_map_timeout_from_callable(self): - # A TimeoutError raised by the mapped callable must not be treated as a - # map() timeout: the remaining calls keep running, like any other - # exception. + # A TimeoutError from the callable is not the map() timeout. i = self.executor.map(timeout_on_one, [0, 1, 2, 3]) self.assertEqual(next(i), 0) self.assertRaises(TimeoutError, next, i)