Quick answer: Print diagnostics to stderr with print(message, file=sys.stderr), or configure logging with a StreamHandler pointed at sys.stderr. Keeping stdout clean matters when another program parses it, while stderr can carry warnings and operational context.

Python has two standard output streams for most command-line programs: stdout for normal output and stderr for diagnostics. Use stderr for warnings, progress messages, validation errors, and logs that should not mix with data written to stdout.
This split matters when another program reads your script output. A pipeline can capture clean JSON, CSV, or plain text from stdout while still showing status messages from stderr. It also makes testing easier because expected data and diagnostic text can be asserted separately.
Think of stdout as the contract of your script and stderr as commentary about that work. If the script is part of automation, anything printed to stdout may be parsed by another tool. Keeping warnings on stderr prevents those warnings from corrupting the output format.
The official print documentation covers the file argument, the sys.stderr documentation describes the standard stream, the logging documentation covers structured diagnostics, and the argparse documentation covers command-line errors.
Print A Message To stderr
The simplest approach is to pass file=sys.stderr to print().
import sys
print("processing started", file=sys.stderr)
print("result: 42")
The first line goes to stderr, while the result goes to stdout. This is the usual pattern for scripts that produce data.
This approach is enough for many small scripts. It keeps the code readable, avoids extra setup, and still gives callers a clean way to redirect normal output and diagnostic output independently.
Write Directly To sys.stderr
sys.stderr.write() is useful when you need exact control over line endings.
import sys
sys.stderr.write("warning: using fallback settings\n")
sys.stderr.flush()
print("done")
write() does not add a newline automatically, so include \n when you want the next message to start on a new line.
Call flush() when the message should appear immediately, such as before a slow network request or a long file-processing step. Immediate flushing is useful in CI logs where buffered text can appear late.

Keep Data And Diagnostics Separate
A command-line script can print machine-readable output to stdout and human-readable notes to stderr.
import json
import sys
record = {"status": "ok", "count": 3}
print("loading input", file=sys.stderr)
print(json.dumps(record))
This lets callers redirect or parse stdout without cleaning diagnostic text out of the data stream.
For example, a caller can save stdout to a file while allowing stderr to remain visible in the terminal. The article avoids shell blocks here because the important idea is the separation of streams, not a specific shell syntax.
Use logging For Larger Scripts
For larger programs, use the logging module. Its default stream for basic configuration is suitable for diagnostics.
import logging
import sys
logging.basicConfig(
level=logging.INFO,
stream=sys.stderr,
format="%(levelname)s: %(message)s",
)
logging.info("starting job")
print("job output")
Logging adds severity levels and consistent formatting. Use print() for small scripts and logging when diagnostics need structure.
Logging is also better when several modules need to report progress. A single logging setup can control level, formatting, and destination without editing every call site later.

Report argparse Errors
argparse sends usage and error messages to stderr. That behavior is helpful because invalid arguments are diagnostics, not normal output.
import argparse
parser = argparse.ArgumentParser(prog="demo")
parser.add_argument("--count", type=int, required=True)
arguments = parser.parse_args(["--count", "3"])
print(arguments.count)
When parsing fails, argparse prints the message to stderr and exits with a non-zero status code.
That design helps command-line tools behave predictably. Successful output can stay machine-readable, while invalid usage still appears clearly for the person running the command.
Test stderr Output
Use contextlib.redirect_stderr() to test diagnostic output from a function.
import contextlib
import io
import sys
def warn_user():
print("warning: check input", file=sys.stderr)
stream = io.StringIO()
with contextlib.redirect_stderr(stream):
warn_user()
print(stream.getvalue().strip())
This keeps the test focused on the diagnostic stream and avoids mixing messages with normal output assertions.
If you test a full command-line program with subprocess.run(), capture both streams and assert them separately. That catches accidental warnings on stdout before they break a pipeline.
When To Use stderr
Use stderr for warnings, errors, progress text, retries, timing notes, and debugging information. Use stdout for the value another tool should consume.
If your script prints JSON, keep every non-JSON message on stderr. That prevents downstream parsers from failing because a warning appeared before the JSON document.
For long-running jobs, flushing stderr can make progress messages appear sooner in terminals and CI logs. For normal short scripts, print(..., file=sys.stderr) is usually enough.
The rule is simple: output that represents the result belongs on stdout; output that explains what happened belongs on stderr.
Following that rule makes scripts easier to compose. People can pipe the result to another command, redirect diagnostics to a log, or inspect failures without losing the actual output.

Print Directly To stderr
sys.stderr is a text stream for diagnostics. Pass it as the file argument to print() so the normal output stream remains available for data that a shell pipeline or test expects.
import sys
print("result: 42")
print("warning: using fallback", file=sys.stderr)
Keep stdout Machine-Readable
A command-line tool can write JSON, CSV, or another protocol to stdout while sending progress and warnings to stderr. This separation prevents a helpful message from corrupting the data consumed by the next process.
import json
import sys
record = {"status": "ok", "count": 3}
print(json.dumps(record))
print("processed 3 records", file=sys.stderr)

Use Logging For Applications
logging adds levels, formatting, timestamps, and configurable handlers. The default StreamHandler writes to stderr, but configure it explicitly when tests or deployment need a stable stream and format.
import logging
import sys
handler = logging.StreamHandler(sys.stderr)
handler.setFormatter(logging.Formatter("%(levelname)s:%(message)s"))
logger = logging.getLogger("worker")
logger.handlers.clear()
logger.addHandler(handler)
logger.setLevel(logging.INFO)
logger.info("job started")
Capture stderr In A Subprocess
When a parent process runs Python, capture stdout and stderr separately so it can parse results without losing diagnostics. Avoid merging both streams when their roles need to remain distinct.
import subprocess
import sys
result = subprocess.run(
[sys.executable, "-c", "import sys; print('data'); print('note', file=sys.stderr)"],
capture_output=True,
text=True,
check=True,
)
print("stdout:", result.stdout.strip())
print("stderr:", result.stderr.strip())
Python’s official sys.stderr documentation defines the standard error stream, and the logging documentation covers configurable diagnostic handlers.
For related command-line diagnostics, compare traceback output, Python logging, and subprocess control when stdout and stderr need separate ownership.
Frequently Asked Questions
How do I print to stderr in Python?
Call print(message, file=sys.stderr) after importing sys.
What is the difference between stdout and stderr?
stdout is commonly used for normal program output while stderr carries diagnostics, warnings, and errors that should remain separate in a pipeline.
Can I redirect stderr from a Python script?
Yes. Shell redirection or subprocess pipes can capture file descriptor 2 separately from standard output.
When should I use logging instead of print()?
Use logging when messages need levels, timestamps, formatting, handlers, or configuration that spans a larger application.