How do you make Python's sys.stdout line-buffered from inside a running program?
answer
- Fix it from inside the running process
- sys.stdout is a text wrapper object
- Mutate the wrapper, do not rebind the name
- A keyword-only setting named line_buffering
basics
~10 sCall sys.stdout.reconfigure(line_buffering=True). It flushes the text layer and then switches it to flush on every newline, so piped output stops stalling, without touching the launch command or any print() call.
solid answer
~40 s`sys.stdout` is an `io.TextIOWrapper` over an `io.BufferedWriter`; CPython sets its `line_buffering` flag at startup from `isatty()`, so behind a pipe it is false and output waits for the block buffer. `io.TextIOWrapper.reconfigure(line_buffering=True)` (Python 3.7+) changes that policy in place: it flushes the stream, then makes every newline-terminated write flush through to the descriptor, leaving encoding and newline settings alone. Because it mutates the object rather than rebinding a name, anything that already captured the stream — a `logging.StreamHandler` built at import time, for instance — gets the new policy too, which rebinding `sys.stdout` to a freshly constructed wrapper would not. Guard it with `isinstance(sys.stdout, io.TextIOWrapper)`, since a harness may have swapped in an `io.StringIO`, which has no `reconfigure`. It changes the text layer only; the buffered layer's block size is unchanged.
code
python · 10 linesimport io
import sys
# On a pipe, sys.stdout starts block-buffered: line_buffering is False.
print("isatty:", sys.stdout.isatty(), "line_buffering:", sys.stdout.line_buffering)
if isinstance(sys.stdout, io.TextIOWrapper):
sys.stdout.reconfigure(line_buffering=True)
print("line_buffering now:", sys.stdout.line_buffering)go deeper
Recall that print() output can sit in a buffer when stdout is not a terminal, and that sys.stdout is an object with settings you can inspect. Knowing sys.stdout.line_buffering exists and can be read is enough at this level.
Be ready to name io.TextIOWrapper.reconfigure(line_buffering=True), explain the text layer versus the buffered layer beneath it, and say why the call flushes first and why encoding is untouched. Interviewers expect the isinstance guard too.
Show the production judgement: where the call belongs so it runs before the first log line, why mutating the stream beats rebinding it when handlers captured the object at import time, and how you verify the change rather than assume it took effect.
Own the strategy question of whether logging should depend on stdout buffering at all. Weigh a one-line in-process switch against fixing the launch contract, routing logs through a handler you control, and the cost of every service carrying its own startup shim.
### The situation this answers A long-running service writes its log through hundreds of `print()` calls. Under a terminal the lines appear as they are produced. Behind a pipe — a log collector, `tee`, a container runtime reading the process's stdout — nothing appears for a long stretch, then a large block arrives at once. You cannot edit the call sites, and you cannot change the launch command, so the startup-time switches are unavailable. The fix has to be made from inside the already-running program. ### What `sys.stdout` actually is `sys.stdout` is not a file descriptor and not a raw stream. It is a three-layer stack: an `io.TextIOWrapper` that encodes `str` to bytes and translates newlines, sitting on an `io.BufferedWriter` that accumulates those bytes, sitting on an `io.FileIO` that owns descriptor 1. Two independent buffering decisions live in that stack, and confusing them is the usual source of wrong fixes. The **text layer** carries a `line_buffering` flag. When it is true, any write containing a newline is followed by a `flush()` on the wrapper, and `TextIOWrapper.flush()` also flushes the buffered layer beneath it, so the bytes actually reach the kernel. CPython decides this flag once, at interpreter startup, from `sys.stdout.isatty()`: true for a terminal, false for a pipe or a redirected file. That single decision is the whole tty-versus-pipe difference at the text layer. The **binary layer** carries a block size instead of a policy. With `line_buffering` false, nothing forces a flush until that block fills, the stream is closed at interpreter shutdown, or something calls `flush()` explicitly. On Python 3.14 `io.DEFAULT_BUFFER_SIZE` is 128 KiB, where it was 8 KiB through 3.13 — sixteen times as much log text can now sit unwritten before the buffer drains on its own. ### The in-process fix ```python import io import sys if isinstance(sys.stdout, io.TextIOWrapper): sys.stdout.reconfigure(line_buffering=True) ``` `io.TextIOWrapper.reconfigure()`, available since Python 3.7, changes a text stream's settings after construction. Crucially it **mutates the existing object**: same identity, same underlying `BufferedWriter`, same descriptor. Settings you do not pass are left alone, so encoding, error handler and newline translation are untouched. As part of applying the new settings it flushes the stream, so whatever was already written is pushed out rather than stranded — you can watch the pending line arrive at the pipe at the instant `reconfigure()` is called, not later. The `isinstance` guard is not decoration. `reconfigure()` is a `TextIOWrapper` method, not part of the general text-stream interface; `io.StringIO` does not have it. If a test harness, a capture shim or an embedding host has already replaced `sys.stdout`, an unguarded call raises `AttributeError` at startup, which is a poor trade for a logging improvement. Placement matters: do it once, as early as possible — the top of the entry module, a wrapper entry point that imports the real one, or a `sitecustomize` module on the path. Only writes after the call get the new policy, though the call itself flushes the backlog. ### Why rebinding is the wrong instinct The reflex is to build a fresh wrapper and assign it: ```python sys.stdout = io.TextIOWrapper(sys.stdout.buffer, line_buffering=True) ``` Two things go wrong. First, rebinding changes a **name**, not the object other code is holding. Anything that captured the stream object earlier — a `logging.StreamHandler(sys.stdout)` constructed at import time, a module that did `from sys import stdout`, a library that stashed the stream in a config object — keeps writing through the old wrapper with the old policy, so half your output is fixed and half is not. `reconfigure()` changes the object itself, so every holder benefits at once. Second, the new wrapper now co-owns the same `BufferedWriter`. If it is ever dropped, its finalizer closes it, closing the shared buffer underneath, and the next write anywhere raises `ValueError: I/O operation on closed file`. If you truly must swap the wrapper, `detach()` the buffer from the old one first and keep exactly one owner alive for the process's lifetime — which is a lot of care to spend on reproducing a one-line method call. ### What it does not change `line_buffering` is a text-layer policy, so the `BufferedWriter`'s block size is untouched; changing that would mean constructing a new buffered layer. A write with no trailing newline still waits, because the flush is triggered by the newline, not by the call. `reconfigure(write_through=True)` is a different knob and not a substitute: it pushes characters into the buffered layer immediately instead of holding them in the text layer, but it never flushes that layer, so a pipe still drains only when the block fills or the process exits. And the change is process-local — a child process inherits the descriptor, not this interpreter's flush policy, so it makes its own tty-versus-pipe decision at its own startup. Finally, `sys.stderr` is line-buffered even when it is not a terminal, so this fix is normally needed for `sys.stdout` alone. You can assert the result rather than hope for it: `sys.stdout.line_buffering` is readable, and a health check or a startup log line that prints it turns "the logs looked stuck" into a fact you can see.
- Would reconfigure(write_through=True) achieve the same thing?No. `write_through` stops the text layer from holding characters, pushing them into the `BufferedWriter` immediately, but it never flushes that buffered layer. Behind a pipe the bytes still sit there until the block fills or the process exits, so the stall is unchanged. `line_buffering=True` is the flag that triggers an actual flush at each newline; `write_through` is about where the characters wait, not about when they are written out.
- Your startup call to sys.stdout.reconfigure raises AttributeError — what happened?Something replaced `sys.stdout` with an object that is not an `io.TextIOWrapper` — commonly an `io.StringIO` or a capture shim installed by a test harness or an embedding host. `reconfigure` is a `TextIOWrapper` method, not part of the general text-stream interface, and `io.StringIO` does not provide it. Guard the call with `isinstance(sys.stdout, io.TextIOWrapper)` so a captured stream degrades to no-op instead of crashing startup.
- Does the call change how much the underlying BufferedWriter holds?No. `line_buffering` is a policy on the text layer: it decides when that layer calls `flush()`, which in turn flushes the layer below. The buffered layer's block size is fixed when it is constructed — `io.DEFAULT_BUFFER_SIZE`, 128 KiB on 3.14, or the device's preferred size — and changing it would mean building a new buffered layer over the raw stream, not reconfiguring the wrapper on top.
Rebinding sys.stdout is like putting a new address on the company website while everyone who already copied the old one keeps posting there; reconfigure changes how mail is handled at the address itself, so it works for senders you never hear about.
saying these in an interview costs you the question
- Insists the process must be relaunched with -u to change stdout buffering
- Rebinds sys.stdout to a new wrapper and loses references captured earlier
- Thinks write_through=True makes a piped stream flush at each newline
- Claims reconfigure resizes the underlying buffered layer's block buffer
- Assumes sys.stdout is always a TextIOWrapper and calls reconfigure unguarded
- Believes reconfigure discards text already written to the stream