subprocess text=True Decode Error Loses returncode (tested)
subprocess.run(..., text=True) decodes the child's output as UTF-8 by default. We measured what happens when a successful child process emits even one byte that isn't valid UTF-8: the whole call raises an exception instead of returning a result, and that exception doesn't tell you whether the process succeeded.
1. One invalid byte crashes the whole call, even on a successful process
import subprocess
result = subprocess.run(["printf", "\xff\xfehello"], capture_output=True, text=True)
Traceback (most recent call last):
...
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte
Measured: printf ran to completion and exited 0 — the two leading bytes are just not valid UTF-8, with four valid ASCII bytes ("hello") right after them. text=True decodes the captured bytes with errors="strict" by default, so the decode fails and subprocess.run() never returns a CompletedProcess at all. The exception is raised instead of being attached to a result.
2. The exception has no returncode, pid, or result object
try:
result = subprocess.run(["printf", "\xff\xfehello"], capture_output=True, text=True)
except UnicodeDecodeError as e:
print(hasattr(e, "result"))
False
print(e.args)
('utf-8', b'\xff\xfehello', 0, 1, 'invalid start byte')
Measured: UnicodeDecodeError is a plain decode error, not a subprocess-aware exception — it carries no .result, .returncode, or .pid. If you only catch this exception, you have no way to tell whether the child process succeeded, failed, or is still running; you'd have to re-run the command without text=True just to find out.
3. The raw bytes are recoverable, just not through the normal result
except UnicodeDecodeError as e:
e.object
b'\xff\xfehello'
Measured: the full raw byte string the child produced is still reachable via the standard UnicodeDecodeError.object attribute (it's a generic decode-error attribute, not specific to subprocess) — so the data isn't lost, but recovering it means catching a decode exception and digging into its internals instead of reading result.stdout normally.
4. Fix: pass errors= explicitly if the child's output encoding isn't guaranteed
result = subprocess.run(["printf", "\xff\xfehello"], capture_output=True, text=True, errors="replace")
result.stdout
'\ufffd\ufffdhello'
result.returncode
0
Measured: adding errors="replace" (or "backslashreplace", "surrogateescape") makes the same call return a normal CompletedProcess with the exit code intact, substituting replacement characters for the undecodable bytes instead of raising. Per the subprocess docs, text=True is shorthand for decoding with the locale's preferred encoding and errors="strict" — for any subprocess whose output isn't guaranteed to be clean text (compilers, package managers, anything touching filenames or user data), set errors= explicitly or capture bytes and decode manually so one stray byte can't erase your exit code.
Frequently asked questions
Why does subprocess.run(text=True) sometimes raise UnicodeDecodeError?
We measured it happening when the child process writes even one byte to stdout/stderr that isn't valid under the default UTF-8 strict decoding — text=True decodes with errors='strict' by default, so any invalid byte raises instead of returning a result.
If subprocess.run(text=True) raises UnicodeDecodeError, can I still get the exit code?
Not from the exception — we confirmed UnicodeDecodeError has no .result, .returncode, or .pid attribute. The CompletedProcess object is never created, so you'd have to rerun the command (e.g. without text=True) to learn the real exit status.
Can I recover the raw output after subprocess.run(text=True) raises UnicodeDecodeError?
Yes — we verified the full raw bytes the child produced are available via the exception's own e.object attribute (a standard UnicodeDecodeError field), even though result.stdout was never populated.
How do I stop subprocess.run(text=True) from crashing on bad bytes?
We confirmed passing errors='replace' (or 'backslashreplace'/'surrogateescape') to subprocess.run() makes it substitute characters for invalid bytes instead of raising, returning a normal CompletedProcess with the correct returncode.
Does capture_output=True without text=True have this problem?
No — we measured that without text=True, subprocess.run() returns the raw bytes unmodified (b'\xff\xfehello'), since no decoding happens at all; the UnicodeDecodeError only occurs when text=True (or encoding=) asks subprocess to decode the output for you.
Written with AI assistance. Measured on 2026-10-03 with Python 3.13.5's built-in subprocess module on Linux: ran subprocess.run() with text=True against a child process that printed one invalid-UTF-8 byte followed by valid ASCII text, and inspected the resulting exception and CompletedProcess objects. PlainHeaders is our extension; Python's subprocess module is built in.