3.15 regression: `Popen.wait(timeout=float("inf"))` raises `TypeError: timeout must be a real number or None` on macOS/BSD (new kqueue wait path)
type-bugstdlibtopic-subprocess
## 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:
```python
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`
(aeedae8b44558ee5c406c8b31287195912c29107, 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:
```python
# 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:
```c
/* 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:
```c
/* 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:
```python
>>> 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:
1. **`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).
2. **`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`
(aeedae8b44558ee5c406c8b31287195912c29107, 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 ]
```
<!-- gh-linked-prs -->
### Linked PRs
* gh-154837
* gh-154891
<!-- /gh-linked-prs -->
1 条评论