Bug report
Bug description
Since 3.15, subprocess.Popen.wait(timeout=...) (and therefore
subprocess.run(..., timeout=...) and Popen.communicate(timeout=...)) fails
on macOS with a misleading TypeError when the timeout is a large-but-legal
number such as float("inf"), sys.maxsize, or 1e10. All of these worked in
3.14 and earlier, and float("inf") is a common "no practical limit" sentinel.
Minimal reproducer:
import subprocess
p = subprocess.Popen(["sleep", "0.1"])
print("wait returned:", p.wait(timeout=float("inf")))
On 3.15.0b4 (macOS 15, arm64, built from the python.org tarball):
Traceback (most recent call last):
File "repro.py", line 4, in <module>
print("wait returned:", p.wait(timeout=float("inf")))
~~~~~~^^^^^^^^^^^^^^^^^^^^^^
File ".../lib/python3.15/subprocess.py", line 1439, in wait
return self._wait(timeout=timeout)
~~~~~~~~~~^^^^^^^^^^^^^^^^^
File ".../lib/python3.15/subprocess.py", line 2283, in _wait
if self._wait_pidfd(timeout) or self._wait_kqueue(timeout):
~~~~~~~~~~~~~~~~~^^^^^^^^^
File ".../lib/python3.15/subprocess.py", line 2256, in _wait_kqueue
events = kq.control([kev], 1, timeout) # wait
TypeError: timeout must be a real number or None, not float
On 3.14.6 the same script prints wait returned: 0.
Affected values (3.15.0b4, macOS):
| timeout |
3.14.6 |
3.15.0b4 |
0.5, 1 |
OK |
OK |
1e10 |
OK |
TypeError: timeout must be a real number or None, not float |
float("inf") |
OK |
TypeError: timeout must be a real number or None, not float |
sys.maxsize |
OK |
TypeError: timeout must be a real number or None, not int |
Note the error message is self-contradictory: the rejected value is a float
(or int). The real error is an OverflowError from the timestamp conversion,
masked by the select.kqueue.control implementation (details below).
("Why not timeout=None?" — None remains the idiomatic way to block
forever and still works. But the regression also rejects large finite
timeouts (1e10, sys.maxsize), which None cannot express, and inf
arises naturally from deadline arithmetic — code computing
timeout = min(user_timeout, remaining_budget) flows math.inf through
cleanly where None would need special-casing at every comparison. All of
these values were accepted by every Python version through 3.14.)
Root cause
All references are to current main
(aeedae8, 2026-07-19), which I verified still
has the identical code as 3.15.0b4.
3.15 introduced an event-driven wait for Popen.wait (gh-83069, #144047):
_wait (Lib/subprocess.py:2283) now calls the new _wait_kqueue
(Lib/subprocess.py:2238) on macOS/BSD, which passes the caller's timeout
unclamped to the syscall wrapper:
# Lib/subprocess.py:2256
events = kq.control([kev], 1, timeout) # wait
kqueue.control converts the timeout with _PyTime_FromSecondsObject, which
raises OverflowError for values that don't fit PyTime_t — but the kqueue
code replaces any conversion failure with the TypeError:
/* Modules/selectmodule.c:2366-2371 */
if (_PyTime_FromSecondsObject(&timeout,
otimeout, _PyTime_ROUND_TIMEOUT) < 0) {
PyErr_Format(PyExc_TypeError,
"timeout must be a real number or None, not %T",
otimeout);
return NULL;
}
This is the only one of the five timeout-conversion sites in
Modules/selectmodule.c that rewrites unconditionally. The sibling sites for
select (line 308), poll (636), devpoll (1009) and epoll (1630) all
guard the rewrite with PyErr_ExceptionMatches(PyExc_TypeError), so they let
the OverflowError through:
/* Modules/selectmodule.c:633-640 (poll; select/devpoll/epoll identical shape) */
if (_PyTime_FromMillisecondsObject(&timeout, timeout_obj,
_PyTime_ROUND_TIMEOUT) < 0) {
if (PyErr_ExceptionMatches(PyExc_TypeError)) {
PyErr_Format(PyExc_TypeError,
"timeout must be a real number or None, not %T",
timeout_obj);
}
Observable contrast on the same 3.15.0b4 build:
>>> select.poll().poll(float("inf"))
OverflowError: timestamp out of range for C PyTime_t
>>> select.select([], [], [], float("inf"))
OverflowError: timestamp out of range for C PyTime_t
>>> select.kqueue().control(None, 0, float("inf"))
TypeError: timeout must be a real number or None, not float # masked
Why 3.14 worked: the old POSIX _wait never handed the user timeout to a
syscall — it busy-waited with delay = min(delay * 2, remaining, .05), so any
finite or infinite timeout was fine.
Linux is affected too (from code inspection; I only have macOS to test):
_wait_pidfd (Lib/subprocess.py:2211) passes timeout * 1000 unclamped to
poller.poll(...) at line 2231. Since poll correctly propagates the
conversion failure (see above — verified on macOS, same C code path),
wait(timeout=float("inf")) on Linux >= 5.3 should raise OverflowError
instead — the right exception type, but still a behavior regression from 3.14,
where these values were accepted.
Expected behavior
p.wait(timeout=float("inf")), wait(timeout=sys.maxsize) etc. behave as in
3.14 and earlier: block until the process exits (or until the — practically
unreachable — deadline), rather than raising.
Suggested fix direction
Two independent pieces:
-
Lib/subprocess.py — clamp the per-syscall wait and retry until the
real deadline. There is stdlib precedent in asyncio, which clamps select
timeouts to MAXIMUM_SELECT_TIMEOUT = 24 * 3600 ("Maximum timeout passed
to select to avoid OS limitations", Lib/asyncio/base_events.py:70-71,
applied at 2029-2030) and relies on re-entering the loop. One subtlety: _wait_pidfd and
_wait_kqueue currently raise TimeoutExpired themselves
(Lib/subprocess.py:2233 and :2261), so a naive clamp inside the helpers
would time out early; the clamped wait needs to loop until endtime (or
return False on a clamped-but-not-expired timeout so _wait falls back
to its existing retry machinery).
-
Modules/selectmodule.c — make the kqueue error rewrite conditional on
PyErr_ExceptionMatches(PyExc_TypeError) like the other four sites, so
out-of-range timeouts surface as OverflowError rather than a
false "timeout must be a real number or None". Worth doing regardless of
(1): the current message is wrong for any user of kqueue.control with a
huge timeout.
CPython versions tested on
3.15.0b4 (regression), 3.14.6 (works); code inspected on current main
(aeedae8, 2026-07-19) — same code, so main
is affected as well.
Operating systems tested on
macOS (15, arm64). The failing path is kqueue-only, so all BSD-family
platforms should be affected identically; Linux >= 5.3 affected via the pidfd
path with OverflowError instead (code inspection, untested).
Output from python -VV
Python 3.15.0b4 (main, Jul 18 2026, 07:46:30) [Clang 22.1.7 ]
Linked PRs
Bug report
Bug description
Since 3.15,
subprocess.Popen.wait(timeout=...)(and thereforesubprocess.run(..., timeout=...)andPopen.communicate(timeout=...)) failson macOS with a misleading
TypeErrorwhen the timeout is a large-but-legalnumber such as
float("inf"),sys.maxsize, or1e10. All of these worked in3.14 and earlier, and
float("inf")is a common "no practical limit" sentinel.Minimal reproducer:
On 3.15.0b4 (macOS 15, arm64, built from the python.org tarball):
On 3.14.6 the same script prints
wait returned: 0.Affected values (3.15.0b4, macOS):
0.5,11e10TypeError: timeout must be a real number or None, not floatfloat("inf")TypeError: timeout must be a real number or None, not floatsys.maxsizeTypeError: timeout must be a real number or None, not intNote the error message is self-contradictory: the rejected value is a float
(or int). The real error is an
OverflowErrorfrom the timestamp conversion,masked by the
select.kqueue.controlimplementation (details below).("Why not
timeout=None?" —Noneremains the idiomatic way to blockforever and still works. But the regression also rejects large finite
timeouts (
1e10,sys.maxsize), whichNonecannot express, andinfarises naturally from deadline arithmetic — code computing
timeout = min(user_timeout, remaining_budget)flowsmath.infthroughcleanly where
Nonewould need special-casing at every comparison. All ofthese values were accepted by every Python version through 3.14.)
Root cause
All references are to current
main(aeedae8, 2026-07-19), which I verified still
has the identical code as 3.15.0b4.
3.15 introduced an event-driven wait for
Popen.wait(gh-83069, #144047):_wait(Lib/subprocess.py:2283) now calls the new_wait_kqueue(
Lib/subprocess.py:2238) on macOS/BSD, which passes the caller's timeoutunclamped to the syscall wrapper:
kqueue.controlconverts the timeout with_PyTime_FromSecondsObject, whichraises
OverflowErrorfor values that don't fitPyTime_t— but the kqueuecode replaces any conversion failure with the TypeError:
This is the only one of the five timeout-conversion sites in
Modules/selectmodule.cthat rewrites unconditionally. The sibling sites forselect(line 308),poll(636),devpoll(1009) andepoll(1630) allguard the rewrite with
PyErr_ExceptionMatches(PyExc_TypeError), so they letthe
OverflowErrorthrough:Observable contrast on the same 3.15.0b4 build:
Why 3.14 worked: the old POSIX
_waitnever handed the user timeout to asyscall — it busy-waited with
delay = min(delay * 2, remaining, .05), so anyfinite or infinite timeout was fine.
Linux is affected too (from code inspection; I only have macOS to test):
_wait_pidfd(Lib/subprocess.py:2211) passestimeout * 1000unclamped topoller.poll(...)at line 2231. Sincepollcorrectly propagates theconversion failure (see above — verified on macOS, same C code path),
wait(timeout=float("inf"))on Linux >= 5.3 should raiseOverflowErrorinstead — the right exception type, but still a behavior regression from 3.14,
where these values were accepted.
Expected behavior
p.wait(timeout=float("inf")),wait(timeout=sys.maxsize)etc. behave as in3.14 and earlier: block until the process exits (or until the — practically
unreachable — deadline), rather than raising.
Suggested fix direction
Two independent pieces:
Lib/subprocess.py— clamp the per-syscall wait and retry until thereal deadline. There is stdlib precedent in asyncio, which clamps select
timeouts to
MAXIMUM_SELECT_TIMEOUT = 24 * 3600("Maximum timeout passedto select to avoid OS limitations",
Lib/asyncio/base_events.py:70-71,applied at 2029-2030) and relies on re-entering the loop. One subtlety:
_wait_pidfdand_wait_kqueuecurrently raiseTimeoutExpiredthemselves(
Lib/subprocess.py:2233and:2261), so a naive clamp inside the helperswould time out early; the clamped wait needs to loop until
endtime(orreturn
Falseon a clamped-but-not-expired timeout so_waitfalls backto its existing retry machinery).
Modules/selectmodule.c— make the kqueue error rewrite conditional onPyErr_ExceptionMatches(PyExc_TypeError)like the other four sites, soout-of-range timeouts surface as
OverflowErrorrather than afalse "timeout must be a real number or None". Worth doing regardless of
(1): the current message is wrong for any user of
kqueue.controlwith ahuge timeout.
CPython versions tested on
3.15.0b4 (regression), 3.14.6 (works); code inspected on current
main(aeedae8, 2026-07-19) — same code, so
mainis affected as well.
Operating systems tested on
macOS (15, arm64). The failing path is kqueue-only, so all BSD-family
platforms should be affected identically; Linux >= 5.3 affected via the pidfd
path with
OverflowErrorinstead (code inspection, untested).Output from
python -VVLinked PRs