CalledProcessError.stderr Is None By Default (tested)
Catching subprocess.CalledProcessError feels like you now have the command's error output to log or inspect. We measured what e.stderr actually contains by default: None — the real error message was never captured at all.
1. The exception's own stderr/stdout are both None by default
import subprocess
try:
subprocess.run(["ls", "/no/such/path/xyz"], check=True)
except subprocess.CalledProcessError as e:
e.stderr
None
e.stdout
None
str(e)
"Command '[...]' returned non-zero exit status 2."
Measured: the command's real message (ls: cannot access '/no/such/path/xyz': No such file or directory) printed straight to the terminal — it was inherited, not captured — while both e.stderr and e.stdout on the caught exception are None, and str(e) only tells you the exit code, never the reason.
2. check_output() has the same gap for stderr
try:
subprocess.check_output(["ls", "/no/such/path/xyz"])
except subprocess.CalledProcessError as e:
e.output
b''
e.stderr
None
Measured: check_output() does capture stdout into e.output, but e.stderr is still None — so a command that fails and writes its explanation to stderr (the common case) still leaves you with nothing useful on the exception object, even using the "capturing" convenience wrapper.
3. capture_output=True actually fixes it
try:
subprocess.run(["ls", "/no/such/path/xyz"], check=True, capture_output=True, text=True)
except subprocess.CalledProcessError as e:
e.stderr
"ls: cannot access '/no/such/path/xyz': No such file or directory\n"
Measured: adding capture_output=True (or manually passing stderr=subprocess.PIPE) redirects the child's stderr into a pipe instead of letting it inherit the parent's terminal, and that captured text is exactly what shows up on e.stderr when the exception is raised. Without it, there's nothing to attach — the output already went somewhere else by the time Python builds the exception.
4. What to actually do
If you plan to log or inspect e.stderr/e.stdout from a CalledProcessError, you must pass capture_output=True (or explicit stdout=subprocess.PIPE/stderr=subprocess.PIPE) on the call that might raise it — catching the exception after the fact can't retroactively capture output that was never redirected in the first place. If you only need the raw text for display and not programmatic error handling, remember the uncaptured output already printed directly to your terminal/log when the child ran, even though the exception itself stayed empty.
Frequently asked questions
Does CalledProcessError automatically contain the command's error message?
No — we measured e.stderr and e.stdout both as None by default. The real error text was inherited straight to the terminal, never captured into the exception.
Does check_output() capture stderr into the exception on failure?
No — we measured e.output holding captured stdout, but e.stderr still None, since check_output() only redirects stdout by default, not stderr.
How do I actually get the failing command's error output on the exception?
We measured that passing capture_output=True (or stderr=subprocess.PIPE) on the original call is what populates e.stderr — you can't retroactively capture output after the fact once the exception is raised.
What does str(CalledProcessError) actually tell you?
Based on what we measured, just the command and its non-zero exit status, e.g. "returned non-zero exit status 2" — never the program's own error message unless you separately captured and logged e.stderr.
Is this subprocess stderr-capture behavior documented?
Yes — the Python docs note that run()/check_output() inherit the parent's file descriptors for stdout/stderr unless you pass capture_output or explicit PIPE arguments. We confirmed the exception object reflects exactly that in Python 3.13.5.
Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5 on Linux: ran a command that fails with check=True, caught CalledProcessError, and inspected e.stderr/e.stdout/str(e) both with default arguments and with capture_output=True, to see what information the exception actually carries. PlainHeaders is our extension; Python's subprocess module is built in.