A Popen timeout raises without stopping the child; the owner must terminate and collect it.
Python subprocess timeout: kill and reap the timed-out child
Operation contract
A receipt import starts a separate interpreter. communicate(timeout=...) limits this one wait, and TimeoutExpired leaves the process alive. The exception path kills it, then calls communicate again to close the pipes and collect its return status. A subprocess that launches its own descendants needs a separately designed process-group policy; killing one PID does not imply a whole tree stopped.
Failure and ownership boundary
The fixture uses a sleeping child so the timeout is reproducible. A launch failure occurs before Popen returns and belongs to a different error path. communicate buffers pipe output in memory: use an output budget or a file for untrusted volume. An OS process supervisor may also need to own shutdown when the parent dies.
Working program
import subprocess
import sys
child = subprocess.Popen(
[sys.executable, "-c", "import time; time.sleep(1)"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
)
try:
child.communicate(timeout=0.02)
timed_out = False
except subprocess.TimeoutExpired:
timed_out = True
child.kill()
child.communicate()
print("timed_out", timed_out)
print("reaped", child.returncode is not None)Output
timed_out True
reaped TrueCosts and limits
Process creation and shutdown dominate this tiny fixture. The wait is bounded, but the second communicate can still retain output; set output limits for an untrusted child.
Common Mistakes
- A timeout exception does not kill the process.
- Calling wait with unread pipes can deadlock when the child fills them.
- Killing the direct child is not a process-tree policy.
Connected lessons
- Python subprocess: argument vectors, exit codes and bounded fixtures
- Python subprocess output: spool bytes before accepting a report
- Python asyncio timeout: cancellation and cleanup ownership
Continue with Python POSIX subprocess timeout: stop the owned process group.
