# How to keep stderr separate from captured stdout in Python

> Capture subprocess stdout while leaving stderr inherited, or capture both streams separately and retrieve their output after a nonzero exit.

Canonical URL: https://www.devobs.io/articles/how-do-i-keep-stderr-diagnostics-separate-from-captured-stdout-in-python/
By: Lucas Vale
Published: 2026-10-18T01:36:36.442Z
Updated: 2026-10-18T01:36:36.442Z
Section: Architecture

Use `subprocess.run()` with `stdout=subprocess.PIPE` and leave `stderr` unset to capture stdout without redirecting stderr. If you need both streams afterward, use `capture_output=True`: it captures stdout and stderr separately rather than combining them. [Stream arguments](https://docs.python.org/3/library/subprocess.html#frequently-used-arguments), [run() reference](https://docs.python.org/3/library/subprocess.html#subprocess.run)

## Capture stdout while leaving stderr inherited

Replace this example command with your program and its arguments:

```python
import subprocess

command = ["your-tool", "--export"]

result = subprocess.run(
    command,
    stdout=subprocess.PIPE,
    text=True,
    check=True,
)

output = result.stdout
```

Each stream has a different destination:

- **stdout** goes into a pipe.
- **stderr** remains inherited because its argument is left at the default, `None`.

The inherited stderr destination is not necessarily a terminal; it may already be redirected elsewhere. The important distinction is that the child’s stderr does not enter the stdout capture pipe. [Stream arguments](https://docs.python.org/3/library/subprocess.html#frequently-used-arguments)

After a successful call, `result.stdout` contains the captured output. Because stderr was not captured, `result.stderr` is `None`. [CompletedProcess reference](https://docs.python.org/3/library/subprocess.html#subprocess.CompletedProcess)

Use this pattern when your program needs to parse stdout while leaving messages written to stderr at their existing destination. These settings route streams, not message types: anything the command writes to stdout still belongs to captured stdout.

## Capture both streams for later inspection

When your application also needs access to stderr, capture both streams. Using the same `command`:

```python
result = subprocess.run(
    command,
    capture_output=True,
    text=True,
    check=True,
)

output = result.stdout
diagnostics = result.stderr
```

For stream routing, `capture_output=True` is equivalent to setting both `stdout=subprocess.PIPE` and `stderr=subprocess.PIPE`. Do not supply `stdout` or `stderr` arguments alongside `capture_output=True`.

`run()` waits for the command to finish. In this example, captured stderr becomes available to your code afterward rather than being displayed as it arrives. [run() reference](https://docs.python.org/3/library/subprocess.html#subprocess.run)

After a successful call, the two streams are available separately as `result.stdout` and `result.stderr`. [CompletedProcess reference](https://docs.python.org/3/library/subprocess.html#subprocess.CompletedProcess)

## Avoid merging or discarding stderr

Two other settings serve different purposes:

- **`stderr=subprocess.STDOUT` merges stderr into stdout’s destination.** If stdout is captured, the capture contains both streams. This defeats stream separation.
- **`stderr=subprocess.DEVNULL` sends stderr to the null device.** It keeps stderr out of stdout, but discards it rather than preserving it for display or inspection.

If you need separate results, configure separate destinations before starting the command rather than trying to classify messages after merging them. [Stream arguments](https://docs.python.org/3/library/subprocess.html#frequently-used-arguments)

## Handle nonzero exit statuses

Stream routing and exit-status handling are separate choices. With `check=True`, a nonzero exit status raises `subprocess.CalledProcessError` instead of returning a result. When both streams were captured, the exception provides their contents through `exc.stdout` and `exc.stderr`, along with `exc.returncode`. [run() reference](https://docs.python.org/3/library/subprocess.html#subprocess.run), [CalledProcessError reference](https://docs.python.org/3/library/subprocess.html#subprocess.CalledProcessError)

This example forwards captured stderr to the parent’s `sys.stderr` before re-raising the exception:

```python
import subprocess
import sys

command = ["your-tool", "--export"]

try:
    result = subprocess.run(
        command,
        capture_output=True,
        text=True,
        check=True,
    )
except subprocess.CalledProcessError as exc:
    if exc.stderr:
        sys.stderr.write(exc.stderr)
    raise
else:
    output = result.stdout
```

With the stdout-only capture pattern, stderr already goes to its inherited destination. In that case, captured stdout is still available as `exc.stdout`, but `exc.stderr` is `None`; there is no captured stderr to forward. [Stream arguments](https://docs.python.org/3/library/subprocess.html#frequently-used-arguments), [CalledProcessError reference](https://docs.python.org/3/library/subprocess.html#subprocess.CalledProcessError)

## Choose text or bytes separately from stream routing

Captured output is bytes by default. These examples use `text=True` to obtain strings using the default text encoding. If the command’s output contract specifies an encoding, set it explicitly—for example, `encoding="utf-8"` for a command documented to emit UTF-8. [run() reference](https://docs.python.org/3/library/subprocess.html#subprocess.run), [CompletedProcess reference](https://docs.python.org/3/library/subprocess.html#subprocess.CompletedProcess)

Stream routing determines where output goes; text-mode and encoding options determine how captured output is represented.

## Source references

- <https://docs.python.org/3/library/subprocess.html#subprocess.run>
- <https://docs.python.org/3/library/subprocess.html#subprocess.CompletedProcess>
- <https://docs.python.org/3/library/subprocess.html#subprocess.CalledProcessError>
- <https://docs.python.org/3/library/subprocess.html#frequently-used-arguments>
