skip to content

Why does Python's print() output appear instantly in a terminal but arrive late when stdout is piped?

level: middleimportance: must knowfreq 60%

answer

  1. Something about the stream is decided at startup
  2. It depends on what stdout is attached to
  3. Terminal versus pipe, two different policies
  4. isatty(), and stderr does not follow the same rule

basics

~20 s

CPython picks sys.stdout's buffering at startup from what the stream is attached to: line-buffered when it is a terminal, block-buffered otherwise. Piped output therefore waits in memory until the block fills, something flushes it, or the interpreter exits cleanly.

solid answer

~40 s

`sys.stdout` is an `io.TextIOWrapper` over a buffered binary stream, and CPython chooses its policy at interpreter startup by asking whether file descriptor 1 is a terminal. Interactive means line-buffered: `sys.stdout.line_buffering` is `True`, and every newline flushes. A pipe or a redirect means block-buffered: nothing reaches the reader until the block fills (`io.DEFAULT_BUFFER_SIZE` is 128 KiB on 3.14), you call `flush()`, or the interpreter finalizes and flushes the standard streams for you. `sys.stderr` is deliberately different — line-buffered even when redirected, since Python 3.9 — which is why a redirected run often shows the traceback but not the prints around it. The knobs are `python -u` and `PYTHONUNBUFFERED=1` for the whole process, `sys.stdout.reconfigure(line_buffering=True)` from inside it, and `flush=True` for one call.

code

console · 2 lines
console
python3 -c "import sys; print(sys.stdout.isatty(), sys.stdout.line_buffering, sys.stderr.line_buffering)"
python3 -c "import sys; print(sys.stdout.isatty(), sys.stdout.line_buffering, sys.stderr.line_buffering)" | cat

go deeper

for a junior

Recall the observable fact and its cause: output looks instant in a terminal and can lag when piped to a file or another command, because Python buffers the stream differently in the two cases.

for a middle

Explain the mechanics precisely — the policy chosen at startup from isatty(), line versus block buffering, stderr's separate rule since 3.9, and the three ways to change it: -u, PYTHONUNBUFFERED and reconfigure.

for a senior

Demonstrate the diagnosis: recognise misordered or missing output as a buffering artefact, know which exit paths flush and which do not, and weigh the syscall cost of running unbuffered against the latency you gain.

for a principal

Own the default for the fleet: which processes run unbuffered, where diagnostics belong, and how you avoid an operational convention that only exists as an undocumented flag in one deployment file.

## The stack under `sys.stdout` `sys.stdout` is not the file descriptor. It is an `io.TextIOWrapper` — the text layer that encodes `str` to bytes — wrapping a buffered binary writer, which wraps the raw descriptor. `sys.stdout.buffer` is that binary layer. Two of the wrapper's attributes describe the policy: `line_buffering` (flush on every newline) and `write_through` (pass characters straight down to the binary layer instead of holding them in the text layer). The binary layer has its own block buffer underneath both. ## The policy is chosen once, at startup, from `isatty()` When the interpreter initializes it inspects each standard stream: * **stdout attached to a terminal** → line-buffered. Interactive users need to see output as it is produced, and a terminal is slow enough that per-line syscalls do not matter. * **stdout attached to anything else** — a pipe, a file, a socket, a log collector → block-buffered. Bulk output is the expected case; the buffer amortizes syscalls across many lines. * **stderr** → line-buffered whether or not it is a terminal. This changed in **Python 3.9**; before that, redirected stderr was fully buffered. Nothing re-evaluates this later. The decision is made from what the descriptor looked like at startup, so a program cannot "become" interactive by noticing a terminal halfway through. ## What that means in practice Run a script in a shell and the output looks immediate. Pipe the same script and it can look hung, then emit everything at once at the end. Nothing is broken — the lines are in the process's own memory, waiting. Three things drain that buffer: it fills, someone calls `flush()`, or the interpreter finalizes normally and flushes the standard streams during shutdown. The third case is the one people rely on without realising, and it is the one that fails. Normal shutdown covers a clean return from `main`, `sys.exit()`, and even an uncaught exception — the traceback is printed and finalization still flushes. It does **not** cover a hard kill from outside the process, an out-of-memory kill, `os._exit()`, or a crash inside a native extension. In those cases the buffered text is simply gone: it was never handed to the kernel. ## The asymmetry that confuses people Because stderr is line-buffered and stdout is not, a redirected run produces its two streams at different rates. A program that prints progress to stdout and a traceback to stderr will show the traceback promptly and the progress lines later, in one blob, at shutdown. If a collector merges the two by arrival time, the story it tells is wrong — the breadcrumbs appear to come *after* the failure they preceded. Recognising that as a buffering artefact rather than a logic bug is exactly what the interviewer is looking for. ## Changing the policy From outside, before the process starts: * `python -u` — forces stdout and stderr unbuffered. Since Python 3.7 this also makes the text layer unbuffered, so `sys.stdout.write_through` becomes `True`. It has no effect on stdin. * `PYTHONUNBUFFERED=1` — identical effect, set in the environment, which is what you use when you do not control the command line. From inside, as early as possible: * `sys.stdout.reconfigure(line_buffering=True)` — flush at every newline while keeping partial writes cheap. Available on `io.TextIOWrapper` since Python 3.7. * `print(..., flush=True)` — one call only. What you cannot do is change the decision for output that was already produced before your code ran, or for bytes a native extension writes directly to the descriptor without going through `sys.stdout`. ## Costs and the sensible default Unbuffered means at least one `write` syscall per write call, and the text layer stops coalescing, so a chatty loop pays for it. Line buffering is the usual middle ground: latency of one line, one syscall per line. Full block buffering is right for bulk data — a program piping megabytes into another process should keep it. A useful rule of thumb: if a human or a supervisor is reading the output as it is produced, make the stream low-latency and accept the syscalls; if the output is a data stream being consumed in bulk, leave the block buffer alone and flush at the points that matter. ## Checking, rather than guessing `sys.stdout.isatty()`, `sys.stdout.line_buffering` and `sys.stdout.write_through` tell you exactly what you have at runtime, and printing them under a pipe versus a terminal is a two-line experiment that settles most arguments about this.

  • A script piped into another command is killed before it finishes. What happens to output it had already printed?
    Whatever is still in the buffer is lost, because it was never handed to the kernel. Normal interpreter shutdown flushes the standard streams — including the path where an uncaught exception ends the program — but an external hard kill, an out-of-memory kill, `os._exit()` or a native crash all skip finalization. Running unbuffered, or flushing at meaningful points, is what makes the output survive.
  • What does python -u actually change, and does it affect stdin?
    It forces stdout and stderr unbuffered: the binary layer stops accumulating and, since Python 3.7, the text layer becomes write-through, so `sys.stdout.write_through` reads `True` and `line_buffering` reads `False`. `PYTHONUNBUFFERED=1` does the same thing from the environment. Neither has any effect on stdin.
  • Why not just call flush() after every write and forget about buffering?
    Because each flush costs at least one `write` syscall, and that is precisely the overhead block buffering exists to amortize. For a program emitting thousands of short lines the difference is measurable. Match the policy to the consumer: low latency where something is watching the output, block buffering where bulk data is being piped onward.

saying these in an interview costs you the question

  • Thinks Python re-checks isatty on every print call
  • Says stdout and stderr are buffered the same way
  • Believes an uncaught exception loses buffered stdout
  • Claims -u also changes stdin buffering
  • Treats delayed piped output as a shell or OS bug
  • Cannot name any way to change the buffering

context