Skip to content

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) #154836

Description

@calvinrp

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:

  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
(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

Metadata

Metadata

Assignees

Labels

stdlibStandard Library Python modules in the Lib/ directorytopic-subprocessSubprocess issues.type-bugAn unexpected behavior, bug, or error

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions