https://github.com/python/cpython/commit/3387b193b1a1f58d9f6efc9ae8890db457f4cc54
commit: 3387b193b1a1f58d9f6efc9ae8890db457f4cc54
branch: main
author: Gregory P. Smith <[email protected]>
committer: gpshead <[email protected]>
date: 2026-10-22T22:59:51-07:00
summary:

gh-160326: Raise ValueError for a NaN timeout in Condition.wait() (GH-160327)

A NaN timeout was treated as zero, which made the deadline loops built
on Condition.wait(), such as Queue.get() and Semaphore.acquire(), spin
without ever timing out.

files:
A Misc/NEWS.d/next/Library/2026-10-22-18-30-00.gh-issue-160326.nAn7Tq.rst
M Lib/test/lock_tests.py
M Lib/threading.py

diff --git a/Lib/test/lock_tests.py b/Lib/test/lock_tests.py
index 691029a1a54f7f..47025c041daf1e 100644
--- a/Lib/test/lock_tests.py
+++ b/Lib/test/lock_tests.py
@@ -562,6 +562,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.
@@ -760,6 +764,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 abac31e25886fa..507d40a1baa275 100644
--- a/Lib/threading.py
+++ b/Lib/threading.py
@@ -363,10 +363,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.

_______________________________________________
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