What does subprocess.run() return, and how do you capture the child's output and fail on a non-zero exit?
answer
- run blocks, then hands you a result object
- Four attributes, two of them often None
- Redirect the streams or capture nothing
- Bytes unless you ask for text
- A bad exit code raises nothing by default
basics
~20 ssubprocess.run() blocks until the child exits and returns a CompletedProcess carrying args, returncode, stdout and stderr. Pass capture_output=True and text=True to get output as str, and check=True to raise CalledProcessError when the exit code is not zero.
solid answer
~40 s`subprocess.run()` builds a `Popen`, waits for the child to finish, and hands back a `subprocess.CompletedProcess` with four attributes: `args`, `returncode`, `stdout` and `stderr`. The last two are `None` unless you redirect, so `capture_output=True` (shorthand for `stdout=subprocess.PIPE, stderr=subprocess.PIPE`) is what makes them real, and `text=True` decodes them to `str` instead of `bytes`. A non-zero exit code is **not** an exception by default: `run()` returns normally and only `returncode` records the failure, which is how a caller silently accepts a broken child. `check=True` raises `subprocess.CalledProcessError`, whose `returncode`, `cmd`, `stdout` and `stderr` attributes carry the child's own error text when you also captured output. A missing program is different again - with the default `shell=False` it raises `FileNotFoundError` before the child ever runs.
code
python · 11 linesimport subprocess
import sys
result = subprocess.run(
[sys.executable, "-c", "print('index rebuilt')"],
capture_output=True,
text=True,
check=True,
timeout=30,
)
print(result.returncode, repr(result.stdout))go deeper
Be ready to write the call from memory: a list of arguments, capture_output=True, text=True, check=True, and then read returncode and stdout off the returned object. Say out loud that stdout is None unless you capture it.
Explain the mechanics behind each keyword: capture_output expands to two PIPE redirections, text decodes with the locale encoding, and check turns the exit code into CalledProcessError carrying cmd, returncode, stdout and stderr.
Show the failure taxonomy at a real call site: OSError when the program cannot be started, CalledProcessError for a bad exit code, TimeoutExpired when you bounded the wait - and explain why capturing output is what makes the resulting alert diagnosable.
Own the convention across services: whether shelling out is allowed at all, what a house wrapper enforces (always a list, always check, always a timeout, output logged once), and how you stop unchecked call sites reappearing in review.
`subprocess.run()` is the high-level entry point added in Python 3.5 and is the call you should reach for by default. `subprocess.call`, `check_call` and `check_output` are older thin wrappers over the same machinery, and `os.system` hands a string to a shell and gives you back only a status code with no access to output. `run()` constructs a `subprocess.Popen`, waits for the child process to exit, and returns a `subprocess.CompletedProcess`. ## What CompletedProcess carries Exactly four attributes: `args` (whatever you passed in), `returncode`, `stdout` and `stderr`. The last two are `None` unless you asked for redirection, and that is the single most common surprise for people new to the module. `capture_output=True` is shorthand for `stdout=subprocess.PIPE, stderr=subprocess.PIPE`. You can also redirect a single stream: `stderr=subprocess.STDOUT` merges the child's error stream into its output stream so you get one interleaved text; `stdout=subprocess.DEVNULL` throws output away. Passing `capture_output=True` together with an explicit `stdout=` or `stderr=` raises `ValueError` - pick one style. ## bytes or str By default the pipes are binary and both attributes come back as `bytes`. `text=True` (added in 3.7; the older spelling `universal_newlines=True` still works) decodes with the locale encoding and turns on universal newlines, so `\r\n` from a Windows-flavoured child arrives as `\n`. When the child's encoding is known, say so explicitly with `encoding="utf-8"`, and consider `errors="replace"` for output you only intend to log. Depending on the ambient locale is how a job that works on a developer laptop dies with `UnicodeDecodeError` inside a minimal container image. The `input=` argument sends data to the child's stdin and then closes it; it must be `bytes` unless text mode is on. ## Exit status is not an exception This is the part interviewers actually probe. A child that exits 1 produces a perfectly ordinary `CompletedProcess`. Code that writes `subprocess.run([...])` and never looks at the result has accepted the failure. `check=True` converts a non-zero exit into `subprocess.CalledProcessError`, and that exception carries `returncode`, `cmd`, `stdout` and `stderr` - which is why `check=True` and `capture_output=True` belong together: the traceback then contains the child's own diagnostic instead of a bare exit code. If you would rather inspect the result first, call `check_returncode()` on the `CompletedProcess` to raise the same exception on demand. On POSIX, a negative `returncode` of `-N` means the child was terminated by signal number N rather than exiting on its own. Starting the program is a separate failure mode from running it. With the default `shell=False`, a program that is not on the path raises `FileNotFoundError` (a subclass of `OSError`) - there is no exit code at all, because nothing ever ran. A hardened call site therefore expects three shapes of failure: `OSError` for launch problems, `CalledProcessError` for a bad exit code, and `subprocess.TimeoutExpired` when you passed `timeout=`. ## A concrete miss A nightly search-index rebuilder invokes a helper script that dies at startup because two of its own modules import each other. Python prints an `ImportError` traceback to stderr and exits with status 1. Without `check=True`, the rebuilder receives a `CompletedProcess`, never reads `returncode`, treats the step as done and publishes an empty index; the failure was loud in the child and completely silent in the parent. With `check=True, capture_output=True, text=True`, a `CalledProcessError` propagates and its `stderr` holds the traceback that explains it. ## Everyday neighbours of these arguments `timeout=` bounds the wall-clock wait. `cwd=` runs the child in another directory without mutating the parent's process-wide working directory, which `os.chdir` would do for every thread at once. `env=` replaces the child's environment wholesale rather than adding to it, so build it from `os.environ.copy()` and mutate that copy unless you genuinely want a bare environment. Finally, remember that `capture_output` buffers the entire output in memory: for a child that emits hundreds of megabytes, redirect `stdout=` to an open file object, or drop down to `Popen` and stream it. Everything described here is current behaviour on CPython 3.14.
- You called subprocess.run without check=True. How would the code notice that the child failed?Read `returncode` on the returned `CompletedProcess` and branch on it, or call `check_returncode()` on the result to raise `CalledProcessError` at a point of your choosing. Ignoring the returned object entirely is the bug: `run()` treats a non-zero exit as an ordinary outcome, so an unchecked call site accepts every failure silently.
- Why can capture_output=True be the wrong choice for a long-running child?It buffers the whole of stdout and stderr in the parent's memory until the child exits, so a chatty child can push the parent's RSS up by the size of its entire output, and you see nothing until it finishes. For large or streaming output, redirect `stdout=` to an open file object, or use `Popen` and read incrementally.
- What exception do you get when the program named in the argument list does not exist?With the default `shell=False`, `FileNotFoundError` - a subclass of `OSError` - raised while starting the child, so there is no exit code and no `CompletedProcess` at all. Under `shell=True` you instead get a normal completion with a shell-generated non-zero code such as 127, because the shell started fine and only the command inside it was missing.
Think of it as sending an errand runner out with a form to fill in: you get the form back with the exit code written on it, but the output boxes stay blank unless you handed over an envelope to collect them in.
saying these in an interview costs you the question
- Claims subprocess.run raises automatically on a non-zero exit code
- Expects result.stdout to be populated without capture_output or a redirect
- Thinks stdout is str by default rather than bytes
- Says os.system is equivalent and gives you the output
- Confuses a missing program with a non-zero exit code