Why can Popen.wait() deadlock when the child's stdout is subprocess.PIPE?
answer
- Both sides are waiting on each other
- The buffer between them is finite
- One party waits, the other writes
- Roughly 64 KiB on Linux
- One call reads and reaps together
basics
~20 sA kernel pipe holds a fixed amount, roughly 64 KiB on Linux. Once the child fills it, the child blocks in write() while the parent blocks in Popen.wait, so neither moves. Popen.communicate drains both pipes and waits in one call.
solid answer
~50 s`subprocess.PIPE` gives you a bounded kernel buffer, not an infinite one. If the parent calls `Popen.wait` before reading `Popen.stdout`, a child that produces more output than the buffer holds blocks on its next `write()`, and the parent blocks waiting for an exit that can never come — a mutual wait with no timeout. `Popen.communicate` exists precisely for this: it writes any input, reads `stdout` and `stderr` concurrently until end-of-file, and only then reaps the child, so no stream can starve another. On POSIX it multiplexes the descriptors with a selector; on Windows it uses reader threads. The same trap catches `proc.stdout.read()` followed by `wait()` when `stderr` is also a pipe — the child can block writing to the stream you are not currently reading. Use `subprocess.run`, or `Popen.communicate`, and keep `Popen.wait` for children whose streams are not pipes.
code
python · 11 linesimport subprocess
import sys
noisy = [sys.executable, "-c", "import sys; sys.stdout.write('x' * 10_000_000)"]
proc = subprocess.Popen(noisy, stdout=subprocess.PIPE)
try:
proc.wait(timeout=2)
except subprocess.TimeoutExpired:
print("child blocked writing, parent blocked in wait")
proc.kill()
proc.communicate()go deeper
Recall the rule rather than the internals: if you asked for subprocess.PIPE you must read it, and the safe default is subprocess.run or Popen.communicate rather than Popen.wait.
Explain the mechanics precisely — a fixed-capacity kernel pipe, the child blocked in write(), the parent blocked in wait(), and how communicate reads both streams concurrently before reaping.
Demonstrate the diagnosis: the hang is data-dependent and passes tests, the parent looks idle rather than crashed, and you confirm it by finding the child blocked in a write while the parent sits in wait.
Own the prevention rather than the cure — a standard spawn helper that never exposes raw Popen.wait with pipes attached, plus a hard timeout on every child, turns an unbounded production hang into a bounded, alertable failure.
## The mechanism When you pass `stdout=subprocess.PIPE`, the `subprocess` module asks the kernel for a pipe: a fixed-size ring buffer with a write end handed to the child and a read end kept by the parent as `Popen.stdout`. "Fixed-size" is the whole story. On Linux the default capacity is 65,536 bytes; on other systems it is often smaller. A pipe never grows to fit its writer and never drops data to make room. When it is full, the writing process blocks inside the `write()` system call until a reader takes bytes out. That back-pressure is a feature — it is what stops a fast producer from exhausting memory — but it means the reader is a required participant. Now consider the parent. `Popen.wait` blocks until the child terminates. It reads nothing. So the sequence is: 1. The child writes; the first ~64 KiB lands in the pipe buffer. 2. The buffer fills; the child's next `write()` blocks. 3. The parent is inside `Popen.wait`, waiting for a child that is blocked in a write the parent will never service. Neither side has a timeout, so the program hangs until something kills it. This is the single most common `subprocess` bug in production Python, and the Python documentation calls it out explicitly in the `Popen.wait` entry. ## Why it survives testing The bug is data-dependent. A command that prints a version banner produces far less than the buffer capacity, so it exits, the pipe is closed with its contents intact, and the parent's later read succeeds. Every unit test passes. The hang appears the first time the child gets talkative — a verbose flag, a warning per input row, a stack trace. A job that quietly succeeded on a five-row sample stalls forever on a 6,800-row batch, and because the parent looks idle rather than crashed, monitoring often reports nothing at all: the worker simply stops taking new work while its supervisor still sees a live process. ## What Popen.communicate actually does `Popen.communicate` is not a convenience wrapper around `Popen.wait`; it is a different algorithm. It sends any `input` you pass to `Popen.stdin`, closes it, then reads `Popen.stdout` and `Popen.stderr` **concurrently** until both reach end-of-file, and finally calls `Popen.wait` to reap the child. On POSIX it drives all three descriptors through a selector so no stream can starve another; on Windows, where selectors do not work on pipes, it starts a reader thread per stream. Because it never sits in a blocking wait while a buffer is filling, the deadlock cannot occur. `subprocess.run` is a thin wrapper over `Popen` plus `communicate`, which is why the high-level call is the safe default. ## The variants that still bite - **Reading one stream while the other is a pipe.** `out = proc.stdout.read()` followed by `proc.wait()` looks careful, but if `stderr` is also `subprocess.PIPE` and the child writes heavily to `stderr`, the child blocks on `stderr` while you block reading `stdout`. Either merge them with `stderr=subprocess.STDOUT`, or use `communicate`, or run a reader per stream. - **Writing a large input before reading.** `proc.stdin.write(big)` can block once the child's own input pipe fills, if the child is not consuming because it is itself blocked writing to a full `stdout` pipe. `communicate(input=...)` interleaves both directions and avoids it. - **Iterating `Popen.stdout` and calling `wait()` inside the loop.** The loop stops feeding the pipe the moment it enters the wait. - **`Popen.poll` is not a fix.** It returns immediately instead of blocking, so a poll loop does not deadlock — but if the loop body does not read the pipes, it spins forever against a child that never exits. ## The line-streaming case Sometimes you genuinely want output as it arrives — progress lines, a log tail — and `communicate`, which returns only at end-of-file, is the wrong shape. Then iterate the stream and wait afterwards: ```python with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as proc: for line in proc.stdout: handle(line) proc.wait() ``` The merge into one stream is what makes this safe: there is no second pipe to starve. If you need the two streams kept apart, give each one its own reader thread. ## What to say in an interview Name the bounded buffer, name both blocked parties, and name the one call that reads and waits together. Then add the detail that separates a middle answer from a senior one: the bug is invisible until the child's output crosses the buffer size, so "it worked in staging" is exactly the symptom you should expect.
- You call proc.stdout.read() and then proc.wait(), with stderr also on a pipe. Is that safe?No. While you block reading `stdout`, the child may fill its `stderr` pipe and block writing to it — so it never reaches end-of-file on `stdout` and your read never returns. The fixes are to merge the streams with `stderr=subprocess.STDOUT`, to use `Popen.communicate`, which reads both concurrently, or to give each stream its own reader thread.
- Does calling Popen.poll in a loop avoid the deadlock?It avoids the parent blocking, but not the hang. `Popen.poll` returns `None` immediately while the child lives, so the loop spins — and if the loop body never reads the pipes, the child stays blocked in `write()` and `poll` returns `None` forever. Polling only helps if each iteration also drains the streams, at which point you have hand-rolled a worse `communicate`.
- How do you stream a child's output line by line without risking the deadlock?Merge the two streams with `stderr=subprocess.STDOUT`, iterate `Popen.stdout` in a `for` loop until it ends, and call `Popen.wait` only after the loop. Because you are reading continuously and there is only one pipe, the buffer never stays full. Use the `Popen` object as a context manager so the descriptors are closed even if the loop raises.
- Why does Popen.communicate use threads on Windows but not on POSIX?The POSIX implementation multiplexes the pipe descriptors with a selector, waiting on whichever becomes ready. Windows pipes are not selectable the same way, so `communicate` starts one reader thread per stream and joins them. The observable semantics are identical — both drain every stream concurrently — but it explains why the call is heavier on Windows and why you may see extra threads while a child runs.
Two people share a small pass-through hatch. One keeps stuffing parcels in; the other stands at the door waiting for them to finish before touching the hatch. The hatch fills, and both stand still forever.
saying these in an interview costs you the question
- Thinks a pipe buffer grows without limit
- Says Popen.wait reads the child's output
- Blames the child program for hanging
- Reads one pipe while the other stays unread
- Offers Popen.poll in a loop as the fix
- Assumes it cannot happen because tests passed