skip to content

What do Python's -u flag and PYTHONUNBUFFERED change about stdout and stderr?

level: middleimportance: must knowfreq 55%

answer

  1. Two spellings of one startup switch
  2. Command-line flag or environment variable
  3. Drops the buffered layer under two streams
  4. Text wrapper becomes write-through
  5. Input stream is explicitly untouched

basics

~20 s

Both force stdout and stderr to be unbuffered: the binary layer under each stream is created without a buffer and the text layer is write-through, so every write reaches the descriptor at once. Neither affects stdin.

solid answer

~40 s

`-u` on the command line and `PYTHONUNBUFFERED=1` in the environment are the same switch, and they are read at interpreter startup, before your code exists. Instead of stacking a buffered writer under the text wrapper, CPython gives `sys.stdout` and `sys.stderr` an unbuffered binary layer and creates the `io.TextIOWrapper` with `write_through=True`, so each `write()` on the text object goes straight down to the descriptor rather than sitting in a block buffer. That makes a piped or captured stream behave like an interactive one, at the price of a syscall per write instead of per block. `sys.stdin` is explicitly unaffected. It is the standard fix for container and CI logs, and the reason `PYTHONUNBUFFERED=1` appears in so many images.

code

console · 3 lines
console
python3 -c "import sys; print(sys.stdout.write_through)" | cat
python3 -u -c "import sys; print(sys.stdout.write_through)" | cat
PYTHONUNBUFFERED=1 python3 -c "import sys; print(sys.stdout.write_through)" | cat

go deeper

for a junior

Recall that -u and PYTHONUNBUFFERED=1 are the same thing and are why container logs appear promptly. Knowing where to put the variable in an image or a job definition is the practical half.

for a middle

Explain the mechanism: the switch is read at startup, drops the buffered layer under stdout and stderr, and makes the text wrapper write-through. Be precise that stdin is untouched.

for a senior

Show judgement about cost. Compare unbuffered writes against line buffering via reconfigure and per-call flush=True, and know that the setting does not follow a subprocess child unless you pass it.

for a principal

Decide the default for a fleet: unbuffered everywhere for log fidelity, or line buffering with an explicit contract on how output is captured, and what that costs high-volume writers.

## One switch, two spellings `python -u script.py` and `PYTHONUNBUFFERED=1 python script.py` do exactly the same thing. The environment variable exists because you often cannot change the command line: a container image, a CI runner, or a process manager launches the interpreter for you, and setting a variable in the environment is the only lever you have. Any non-empty value works; the variable is a flag, not a size. Both are consumed during interpreter initialisation, when CPython constructs the standard streams. That timing matters. By the time your module body runs, the decision is already made, and the only way to change it from inside the process is to rebuild or reconfigure the stream. ## What it actually changes Normally `sys.stdout` is a three-layer stack: a raw file object, an `io.BufferedWriter`, and an `io.TextIOWrapper`. Under unbuffered mode CPython skips the buffered layer for the writable streams and builds the text wrapper with `write_through=True`, which means the wrapper does not hold encoded bytes of its own either. The result is that a single `print()` becomes a write to the descriptor immediately, with no waiting for a newline and no waiting for a block to fill. You can see the change without guessing: ```console python3 -c "import sys; print(sys.stdout.write_through, sys.stdout.line_buffering)" | cat python3 -u -c "import sys; print(sys.stdout.write_through, sys.stdout.line_buffering)" | cat ``` The first prints `False False` on a pipe, the second `True False`. Note the second value: unbuffered mode does not set `line_buffering`, it makes line buffering irrelevant, because nothing is being held back to flush in the first place. ## What it does not change **stdin.** The documentation is explicit that the option has no effect on `sys.stdin`. The text layer needs a buffered binary stream underneath it, so input stays buffered whatever you pass. **Child processes.** The setting belongs to one interpreter. If your program launches another Python program through `subprocess.Popen`, the child re-reads its own environment and its own command line. A child started with a plain `python` and an inherited but unset variable will block-buffer into the pipe you gave it, no matter what the parent did. Passing `PYTHONUNBUFFERED=1` in the child's environment, or adding `-u` to its argument list, is how you fix that. **Non-Python programs.** C programs make their own buffering decisions through the C runtime; a Python-level switch does nothing for them. ## Cost, and the milder alternative Unbuffered output means one `write(2)` per write call, and `print()` with its default separator and end can issue more than one write per call. For a program emitting a handful of lines a second that is free. For one emitting a hundred thousand lines into a file it is measurable, and the syscalls are all synchronous. The usual middle ground is line buffering rather than no buffering: you still get one flush per line, which is what a log reader actually wants, but multi-write `print()` calls coalesce. From inside the program that is `sys.stdout.reconfigure(line_buffering=True)`, run once early. From outside, there is no flag for it, which is exactly why `-u` is the one people reach for. The third option is per-call: `print(..., flush=True)` on the writes that matter. That is right when only a few statements are latency-sensitive, such as a progress marker inside a long loop, and wrong as a habit sprinkled over every call site. ## Interaction with stderr Since Python 3.9 `sys.stderr` is line buffered even when it is not a terminal, so it already escapes promptly and `-u` changes less for it than people expect. What `-u` adds for stderr is that output appears without waiting for the newline, which matters for partial-line progress writes and for the moment just before a hard crash. ## The interview answer in one breath They are the same switch, read at startup; they drop the buffered layer under stdout and stderr and make the text wrapper write-through; they leave stdin alone; they do not propagate to child processes; and their cost is a syscall per write, which is why line buffering is often the better answer when you control the code.

  • Your parent process runs with -u and still sees the child's output arrive in bursts. Why?
    Because buffering is a property of the writing interpreter, not of the pipe or the reader. The child made its own decision at its own startup, saw a pipe on its stdout, and chose block buffering. Fix it at the child: add `-u` to its argv or put `PYTHONUNBUFFERED=1` in the environment you pass to `subprocess.Popen`. Nothing the parent does to its own streams can help.
  • When would you prefer line buffering over fully unbuffered output?
    Whenever the consumer is line-oriented, which is almost always. Line buffering gives the same practical latency, one flush per completed line, but lets a single `print()` that issues several writes coalesce into one syscall, and it avoids splitting a line across two writes. `sys.stdout.reconfigure(line_buffering=True)` sets it from inside the program; there is no command-line flag for it, which is why `-u` gets used as a blunt substitute.
  • Does -u guarantee your logs survive a hard kill?
    For anything already written, yes, because there is no userspace buffer left holding it; the bytes are in the pipe or file as soon as the write returns. It says nothing about what happens downstream, though: a collector reading the pipe has buffers of its own, and a line written a microsecond before the kill can still be in flight. Unbuffered removes your buffer, not everyone's.

saying these in an interview costs you the question

  • Thinks PYTHONUNBUFFERED takes a buffer size
  • Believes the flag also unbuffers stdin
  • Expects the setting to propagate to child processes
  • Says it can be set from inside the running program
  • Claims unbuffered output is free of cost
  • Confuses unbuffered with line buffered

context