Skip to content

subprocess.run(timeout=...) swallows a signal handler's InterruptedError on macOS in Python 3.15 #160700

Open

Description

@nick-inkeep

Bug report

On macOS, a timed subprocess.run() does not propagate an exception raised by a Python signal handler while the child is still running. In Python 3.15.2 the handler below raises InterruptedError, but run() returns the child's exit status after the child finishes. Python 3.14.10 raises the exception immediately.

import os
import signal
import subprocess
import sys
import time


def handler(signum, frame):
    signal.signal(signal.SIGTERM, signal.SIG_IGN)
    raise InterruptedError(f"interrupted by signal {signum}")


signal.signal(signal.SIGTERM, handler)
started = time.monotonic()
child = "import os, time; os.kill(os.getppid(), 15); time.sleep(5)"
try:
    result = subprocess.run([sys.executable, "-c", child], timeout=60)
    print("returned", result.returncode, "after", round(time.monotonic() - started, 2), "seconds")
except BaseException as error:
    print("raised", type(error).__name__, "after", round(time.monotonic() - started, 2), "seconds")

On macOS 26.7 arm64 (Darwin 25.6.0 arm), repeated five times:

  • Python 3.15.2 (v3.15.2, commit 5e58eaad7c58df58dfa8ea549c87996e01dd1077): returned 0 after 5.07–5.08 seconds (5/5).
  • Python 3.14.10 (commit b703b2c5e704341d61e367b9b940b77ed11dded0): raised InterruptedError after 0.01–0.03 seconds (5/5).
  • CPython main at c436e8d83597c6f8c1bc7e37812c98d46670c29a (Python 3.16.0a1+dev): returned 0 after 5.04–5.08 seconds (5/5).

The expected result is for the InterruptedError raised by the handler to escape the timed wait, as it does in 3.14.10. PEP 475 specifies that when a Python signal handler raises, the interrupted system-call wrapper must fail with that exception: https://peps.python.org/pep-0475/#proposal.

CPython versions tested on:

3.15.2, 3.14.10, and CPython main (built at c436e8d83597c6f8c1bc7e37812c98d46670c29a; Python 3.16.0a1+dev).

Operating systems tested on:

macOS 26.7 arm64.

Additional information

At v3.15.2, Lib/subprocess.py:2279-2282 catches OSError around kq.control() and returns False; Python documents InterruptedError as an OSError subclass, so this catch covers the handler exception and _wait() falls back to polling instead of propagating it. The same catch is present on CPython main at Lib/subprocess.py:2303-2306:

cpython/Lib/subprocess.py

Lines 2303 to 2306 in c436e8d

try:
events = kq.control(changelist, 1, max(delay, 0))
except OSError:
return False
.

The release code is at

cpython/Lib/subprocess.py

Lines 2279 to 2282 in 5e58eaa

try:
events = kq.control(changelist, 1, max(delay, 0))
except OSError:
return False
. The 3.15 maintenance branch has the same source at Lib/subprocess.py:2279-2282. The existing test_kqueue_control_error only exercises generic OSError fallback with EPERM at Lib/test/test_subprocess.py:4217-4231; it does not distinguish InterruptedError. The select.kqueue.control() documentation specifies that a signal-handler-raised exception is not retried at Doc/library/select.rst:535-538. Popen._internal_poll() has a separate broad OSError catch around waitpid(WNOHANG) at Lib/subprocess.py:2190; it should be audited for the same signal-handler exception issue.

Linked PRs

Activity

  1. added
    type-bugAn unexpected behavior, bug, or error
    stdlibStandard Library Python modules in the Lib/ directory
    on Oct 26, 2026
  2. added a commit that references this issue on Oct 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    OS-macstdlibStandard 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