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.