diff --git a/Lib/concurrent/futures/_base.py b/Lib/concurrent/futures/_base.py index cc335d9aa1ea55d..4cf52f87bdd761d 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 5d9f27c83bf9a81..1b1eac639453687 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,16 @@ 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. + 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 000000000000000..57be15bad739238 --- /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.