diff --git a/Lib/test/lock_tests.py b/Lib/test/lock_tests.py index fb11f4828957ff..1a41fd3f1ddb4d 100644 --- a/Lib/test/lock_tests.py +++ b/Lib/test/lock_tests.py @@ -557,6 +557,10 @@ def f(): for r, dt in results2: self.assertTrue(r) + def test_nan_timeout(self): + evt = self.eventtype() + self.assertRaises(ValueError, evt.wait, float('nan')) + def test_set_and_clear(self): # gh-57711: check that wait() returns true even when the event is # cleared before the waiting thread is woken up. @@ -755,6 +759,14 @@ def f(): # In practice, this implementation has no spurious wakeups. self.assertFalse(result) + def test_nan_timeout(self): + cond = self.condtype() + nan = float('nan') + with cond: + self.assertRaises(ValueError, cond.wait, nan) + self.assertRaises(ValueError, cond.wait_for, lambda: False, nan) + self.assertEqual(len(cond._waiters), 0) + def test_waitfor(self): cond = self.condtype() state = 0 diff --git a/Lib/threading.py b/Lib/threading.py index c03b0b5370c933..01274f1b857eee 100644 --- a/Lib/threading.py +++ b/Lib/threading.py @@ -369,10 +369,11 @@ def wait(self, timeout=None): waiter.acquire() gotit = True else: - if timeout > 0: - gotit = waiter.acquire(True, timeout) - else: + if timeout <= 0: gotit = waiter.acquire(False) + else: + # A NaN timeout is rejected by acquire(). + gotit = waiter.acquire(True, timeout) return gotit finally: self._acquire_restore(saved_state) diff --git a/Misc/NEWS.d/next/Library/2026-10-22-18-30-00.gh-issue-160326.nAn7Tq.rst b/Misc/NEWS.d/next/Library/2026-10-22-18-30-00.gh-issue-160326.nAn7Tq.rst new file mode 100644 index 00000000000000..4349a7380c377d --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-22-18-30-00.gh-issue-160326.nAn7Tq.rst @@ -0,0 +1,5 @@ +Raise :exc:`ValueError` for a NaN *timeout* in +:meth:`threading.Condition.wait` and the blocking calls built on it, such as +:meth:`threading.Event.wait`, :meth:`threading.Semaphore.acquire` and +:meth:`queue.Queue.get`, instead of returning at once or looping without ever +timing out.