Why is stdout=subprocess.DEVNULL safer than subprocess.PIPE for output you never read?
answer
- One choice makes you a required reader
- Two different file descriptors for the child
- Bounded kernel buffer versus the null device
- Discarded output cannot fill anything
- CompletedProcess.stdout is None without a pipe
basics
~20 ssubprocess.DEVNULL points the child at the null device, so its writes always succeed and vanish. subprocess.PIPE creates a kernel pipe with a fixed capacity; once it fills, the child blocks until the parent reads it.
solid answer
~50 sBoth constants are just sentinel values you pass to `subprocess.run` or `subprocess.Popen` for `stdout`/`stderr`, but they set up completely different plumbing. `subprocess.DEVNULL` makes the child's descriptor point at the operating system's null device: every write is accepted and discarded, the child never blocks, and there is nothing for the parent to drain. `subprocess.PIPE` creates a kernel pipe whose buffer is finite — typically 64 KiB on Linux and smaller on some systems — and the parent becomes responsible for emptying it. If you asked for a pipe and then never read it, a chatty child fills the buffer and blocks inside `write()` while you sit in `Popen.wait`, and neither side moves. So the rule is: ask for a pipe only when you actually intend to consume the bytes; otherwise use `subprocess.DEVNULL`, or point the descriptor at a file.
code
python · 6 linesimport subprocess
import sys
noisy = "import sys; sys.stdout.write('x' * 500_000)"
done = subprocess.run([sys.executable, "-c", noisy], stdout=subprocess.DEVNULL)
print(done.returncode, done.stdout)go deeper
Recall that subprocess.DEVNULL throws output away and subprocess.PIPE captures it into a buffer you must read. Be ready to say which one you would pass when you only care about the exit status.
Explain the mechanics: a pipe is a bounded kernel buffer, roughly 64 KiB on Linux, and a full one blocks the child's write. Explain that omitting the argument entirely means inheritance, not silence.
Show the production judgement — discard stdout but keep stderr on a pipe so failures stay diagnosable, close stdin with subprocess.DEVNULL so a prompting command cannot stall a batch job, and watch descriptor leaks in long-lived spawners.
Own the convention: a house rule that every spawn site declares all three streams explicitly, with pipes only where a reader exists, removes an entire class of production hang from the codebase without anyone having to reason about buffer sizes.
## The two constants are plumbing choices, not formatting options `subprocess.PIPE`, `subprocess.DEVNULL` and `subprocess.STDOUT` are negative integer sentinels (-1, -3 and -2) that the `subprocess` module recognises in the `stdin`, `stdout` and `stderr` arguments. They are not values the child ever sees; they tell the parent what file descriptor to install as the child's descriptor 0, 1 or 2 before it execs. - `subprocess.DEVNULL` opens the platform's null device and dups it onto the child's descriptor. Writes to it are accepted and thrown away by the kernel, and they never block. There is no buffer to fill and no reader to schedule. - `subprocess.PIPE` calls the kernel to create a pipe, gives the write end to the child and keeps the read end in the parent as `Popen.stdout` (or `Popen.stderr`). A pipe is a bounded ring buffer inside the kernel. On Linux the default capacity is 64 KiB; other systems use less. When it is full, the child's next `write()` blocks until somebody reads. - Passing nothing at all is a third option and a common source of confusion: the child then **inherits** the parent's own descriptor, so its output goes straight to the parent's terminal or log file. That is also non-blocking from your perspective, but it is not silence. ## Why the pipe choice is the risky one The danger of `subprocess.PIPE` is that it silently makes the parent a required participant. The moment you request a pipe you have signed a contract: someone in this process must read that descriptor until end-of-file, or the child will eventually stall. For a command that prints one line, nothing bad happens — one line fits in 64 KiB, the child exits, and you can read the pipe at your leisure afterwards. That is exactly why the bug hides: it passes every test against a quiet command and hangs the first time the command gets talkative. `subprocess.DEVNULL` removes the contract entirely. The child can emit a gigabyte of progress noise and the parent has nothing to do. ## When you want silence, and when you don't Use `subprocess.DEVNULL` when the output genuinely has no value: a tool that prints a progress bar you do not want in your logs, a command run purely for its side effect and exit status, or a child whose `stdin` you want to close off (`stdin=subprocess.DEVNULL` gives it an immediate end-of-file instead of leaving it attached to your terminal, which stops an interactive prompt from silently hanging a batch job). Do **not** reach for it when you would want the text during an incident. Discarding `stderr` is the classic regret: the command fails, `subprocess.run` raises `subprocess.CalledProcessError`, and the one thing that would have explained why is gone. A frequent middle ground is `stdout=subprocess.DEVNULL, stderr=subprocess.PIPE` — throw away the noise, keep the diagnosis — which is safe precisely because a diagnostic message is small. If you want everything but do not want it in memory, redirect to a real file object: `stdout=open(path, "wb")`. The child then writes to a regular file descriptor, which has no fixed capacity, so it cannot block on a full buffer, and you can read the file afterwards. ## What each choice leaves you holding With `subprocess.run`, the returned `subprocess.CompletedProcess` has `stdout` set to `None` unless you asked for a pipe — that is not a bug, it is the direct consequence of never having captured anything. `subprocess.DEVNULL`, an inherited descriptor and a file redirection all leave `CompletedProcess.stdout` as `None`. Only `subprocess.PIPE` (or the `capture_output=True` shorthand, which sets both streams to pipes) fills it in. One more consequence: with `subprocess.PIPE` the parent holds the read end open. If you never close `Popen.stdout` and never let the `Popen` object be garbage collected, you leak a descriptor per child, and a long-lived process that spawns thousands of children will eventually hit its descriptor limit. Using `Popen` as a context manager, or calling `Popen.communicate`, closes the pipes for you. `subprocess.DEVNULL` leaves nothing to close. ## The rule to carry Ask for a pipe only when you will read it, and read it with a call designed to read it. Otherwise send the stream to `subprocess.DEVNULL` or to a file, and keep the ability to hang out of your program entirely.
- What happens if you pass neither subprocess.PIPE nor subprocess.DEVNULL for a child's stdout?The child inherits the parent's own descriptor 1, so its output lands wherever the parent's stdout goes — the terminal, the parent's log file, or a systemd journal. Nothing blocks, but nothing is captured either, and `CompletedProcess.stdout` is `None`. That is often what you want for a build tool run interactively, and almost never what you want for a service, where the child's chatter ends up interleaved in your structured logs.
- Why would you set stdin=subprocess.DEVNULL on a child you are not writing to?By default the child inherits the parent's stdin. If the command decides to prompt — for a password, a confirmation, a missing argument — it will block reading a descriptor that nobody will ever type into, and the job hangs with no output explaining why. `subprocess.DEVNULL` on stdin gives the child an immediate end-of-file, so a prompting command fails fast instead of stalling.
- Is subprocess.STDOUT a third kind of destination?No — `stderr=subprocess.STDOUT` means "make the child's descriptor 2 a duplicate of whatever descriptor 1 already is". It merges the two streams into one destination, whether that destination is a pipe, a file or the inherited terminal. It is a merge instruction, not a new sink, and merging costs you the ability to tell error text from normal output afterwards.
A pipe is a small mail slot: if nobody empties it the sender jams against a full slot. The null device is a shredder bolted to the wall — it accepts mail forever and asks nothing of you.
saying these in an interview costs you the question
- Thinks subprocess.PIPE is free and always safe
- Believes a pipe grows to hold any amount of output
- Uses subprocess.PIPE then never reads the stream
- Discards stderr, leaving failures undiagnosable
- Thinks omitting stdout silences the child
- Confuses subprocess.STDOUT with a discard sink