skip to content

What does sys.__stdout__ hold, and how does it differ from sys.stdout?

level: juniorimportance: should knowfreq 33%

answer

  1. One name is live, one is frozen
  2. A snapshot taken at interpreter startup
  3. Nothing in the stdlib updates it
  4. It may be None with no console attached
  5. Still writes to descriptor 1

basics

~20 s

sys.stdout keeps the stream object the interpreter attached at startup and is never updated afterwards. sys.stdout is the current one, which any library or context manager may have replaced. Use the dunder name to get back to the original.

solid answer

~50 s

At interpreter startup both names point at the same text stream wrapping file descriptor 1. From then on `sys.stdout` is live — anything can assign to it, and `contextlib.redirect_stdout` does exactly that for the length of a `with` block — while `sys.__stdout__` is a frozen snapshot that nothing in the standard library updates. Its purpose is recovery: if a library reassigns `sys.stdout` and forgets to restore it, `sys.stdout = sys.__stdout__` puts things back, and a debugger or crash handler can use it to reach the real console regardless of what user code has done. Two caveats. It can be `None` when the interpreter starts with no standard streams attached, so guard before using it. And it is not an escape from descriptor-level redirection: it still wraps descriptor 1, so if `os.dup2` moved that number, writes to it follow.

code

pycon · 7 lines
pycon
>>> import contextlib, io, sys
>>> buf = io.StringIO()
>>> with contextlib.redirect_stdout(buf):
...     print(sys.stdout is buf, sys.__stdout__ is buf)
...
>>> buf.getvalue()
'True False\n'

go deeper

for a junior

Recall the one-line distinction: sys.stdout is the stream in use right now, sys.stdout is the one the interpreter set up at startup and never changes. Knowing you can assign the second to the first to recover is enough.

for a middle

Explain why the pair exists and where the snapshot is genuinely useful — recovery, a debugger reaching the console, testing whether a stream was substituted — and note that it can be None when no console is attached.

for a senior

Show the limit of the guarantee: the dunder name survives object substitution but not a descriptor swap, because it still writes to descriptor 1. Say what that means for a crash handler that must produce output whatever a dependency has done.

for a principal

Take a position on convention. A component that writes to sys.stdout to escape capture is opting out of every downstream capture policy; decide when that is legitimate for operator-facing output and when it is a library overstepping its host.

## What the interpreter sets up at startup During initialisation CPython opens the three standard streams and binds each one under two names on the `sys` module: `sys.stdout` and `sys.__stdout__`, `sys.stderr` and `sys.__stderr__`, `sys.stdin` and `sys.__stdin__`. Immediately after startup each pair refers to the *same* object — a buffered text wrapper sitting on top of file descriptor 1 for output. The difference is what happens next. `sys.stdout` is a normal, writable module attribute that any code may reassign, and plenty of code does: test runners install a capture buffer, `contextlib.redirect_stdout` swaps in your target for the duration of a `with` block, a notebook front end substitutes a stream that forwards to a browser. `sys.__stdout__` is left alone by all of it. Nothing in the standard library reassigns the dunder name, so it remains the object the interpreter itself installed. So the mental model is: **`sys.stdout` is the current stream; `sys.__stdout__` is the original one.** The double-underscore name is not magic and there is no protocol behind it — it is simply a place the interpreter parked a reference so it can always be found again. ## What it is for The honest uses are narrow, and an interviewer is checking that you know them rather than that you use the name daily. **Recovery.** If a library reassigns `sys.stdout` and an exception prevents it from restoring, output disappears for the rest of the process. `sys.stdout = sys.__stdout__` is the way back. (The better fix is that the library should have used a context manager, so the restore happens on the way out even on an exception.) **Reaching the real console deliberately.** A debugger prompt, an interactive crash handler or a progress display wants the user's terminal, not whatever capture some framework has installed. Writing to `sys.__stdout__` bypasses the object-level substitution. **Detecting a substitution.** `sys.stdout is sys.__stdout__` is a cheap test for "has anything swapped my output stream?", which is occasionally useful when deciding whether a stream is safe to interrogate. ## The two caveats that make it an interview question **It can be `None`.** These attributes are only meaningful when the interpreter actually had standard streams to attach. A process started by a GUI-mode launcher, or one whose descriptors were closed before the interpreter came up, can have `sys.__stdout__` set to `None`. Code that falls back to it — a logging shim, a crash reporter — must check for `None` rather than assume a file-like object, or it fails in exactly the deployment where you least want a second exception. **It does not survive descriptor-level redirection.** This is the sharper point and the one that ties this leaf together. `sys.__stdout__` is a Python object that writes to file descriptor 1. If someone used `os.dup2` to point descriptor 1 at a temporary file, writes to `sys.__stdout__` go into that file exactly like writes to `sys.stdout`, because both objects address the same number. The dunder name protects you against *object* substitution only. The only route back to the original destination after a descriptor swap is the duplicate that the swapping code saved with `os.dup`, or opening the controlling terminal directly. ## A concrete trace ```pycon >>> import contextlib, io, sys >>> buf = io.StringIO() >>> with contextlib.redirect_stdout(buf): ... print(sys.stdout is buf, sys.__stdout__ is buf) ... >>> buf.getvalue() 'True False\n' ``` Inside the block `sys.stdout` is the buffer and `sys.__stdout__` is not — which is why the `print()` output ended up in `buf` and had to be read back afterwards. ## What a good answer avoids Do not describe `sys.__stdout__` as "the real stdout" in a way that implies the operating system knows about it; the kernel only knows descriptor numbers. Do not treat it as the normal way to print — writing to it deliberately defeats test capture, and a test that mysteriously sees output on the console is usually a library reaching for the dunder name when it should not have. And do not confuse it with the saved value that `contextlib.redirect_stdout` keeps internally: that saved value is whatever `sys.stdout` happened to be on entry, which in a nested redirect is another substitute, not the startup stream.

  • A library reassigned sys.stdout and never restored it. How do you recover?
    Assign sys.stdout = sys.__stdout__, after checking that it is not None. That works because the dunder attribute still refers to the object installed at interpreter startup. The durable fix is to make the swap a context manager — contextlib.redirect_stdout, or your own class with the restore in __exit__ — so an exception cannot skip the restore.
  • After os.dup2 points descriptor 1 at a file, does writing to sys.__stdout__ still reach the terminal?
    No. sys.__stdout__ is an object wrapping descriptor 1, and that number now refers to the file, so the bytes land there. The dunder name only rescues you from object-level substitution. Getting back to the terminal requires the duplicate saved with os.dup before the swap, or opening the controlling terminal device directly.
  • When would sys.__stdout__ be None?
    When the interpreter started without standard streams attached — a GUI-mode launcher with no console, or a process whose descriptors were closed before the interpreter initialised. Any fallback path that reaches for it, such as a crash reporter or a logging shim, must handle None instead of assuming a file-like object.

saying these in an interview costs you the question

  • Thinks sys.__stdout__ tracks the current stream
  • Assumes it is always a usable file-like object
  • Believes writing to it bypasses descriptor redirection
  • Uses it as the normal way to print
  • Confuses it with the value redirect_stdout saves internally
  • Says the operating system knows about the dunder name

context