You wrote what looks like perfectly reasonable Python code:
| |
And it hangs. Forever. No error, no timeout, just… nothing.
Welcome to the pipe buffer deadlock, one of Python’s most frustrating subprocess gotchas.
Why This Happens
The problem is OS pipe buffers. When you create a subprocess with stdout=subprocess.PIPE, the OS creates a pipe with a fixed buffer size (typically 64KB on Linux).
Here’s the sequence that causes the hang:
- Your subprocess writes to stdout
- The output goes into the pipe buffer
- The buffer fills up (64KB)
- The subprocess blocks, waiting for someone to read the pipe
- Your Python code is still waiting on
proc.stdout.read() - Deadlock: subprocess waits for Python to read, Python waits for subprocess to finish
If your command produces more than 64KB of output, you’re stuck.
The Fix: Use communicate()
The communicate() method exists specifically to solve this problem:
| |
communicate() reads from both stdout and stderr simultaneously, preventing the buffer from filling up. It returns when the process exits and all output has been collected.
With a Timeout
| |
When communicate() Isn’t Enough
Sometimes you need to process output as it arrives—for progress bars, real-time logging, or very large outputs that shouldn’t live in memory.
Option 1: Read Line by Line
| |
Warning: This still deadlocks if stderr fills up while you’re reading stdout. For safety, redirect stderr:
| |
Option 2: Use Threading
If you need both streams separately in real-time:
| |
Option 3: Use asyncio (Python 3.8+)
The cleanest solution for async codebases:
| |
Quick Reference: Which Method to Use
| Situation | Solution |
|---|---|
| Simple command, get all output | communicate() |
| Need timeout | communicate(timeout=N) |
| Process output line by line | for line in proc.stdout + merge stderr |
| Need both streams in real-time | Threading with queues |
| Already using asyncio | asyncio.create_subprocess_exec() |
| Output too large for memory | Stream to file, then process |
The subprocess.run() Shortcut
For simple cases, skip Popen entirely:
| |
subprocess.run() uses communicate() internally, so it handles the buffer issue automatically.
Debugging Tips
If you’re still seeing hangs:
Check for input: Does your command expect stdin? Pass
stdin=subprocess.DEVNULLif not.Test output size: Run the command manually and check output size with
command | wc -c.Add logging: Print before and after each subprocess operation to find where it sticks.
Check for prompts: Some commands prompt for confirmation. Use
yes |prefix or pass appropriate flags.
| |
Summary
The subprocess deadlock happens because OS pipe buffers are finite. When they fill up, everything blocks. The fix is almost always communicate()—it handles the complexity of reading from multiple pipes simultaneously.
Save yourself the debugging headache: default to subprocess.run() with capture_output=True for simple cases, and reach for Popen + communicate() only when you need more control.