skip to content

Standard Streams & StringIO

The $stdout and STDOUT pairs and why you reassign the global, gets reading ARGF, sync and flush, and StringIO as an in-memory stream. Interviewers use it to test capturing output in tests.

on this pageshow

explore

questions

6

In Ruby, what is the difference between $stdout and STDOUT, and which one should you reassign to redirect output?

level: juniorimportance: must knowfreq 58%

answer

  1. one is a variable, one a constant
  2. Kernel#puts consults the global
  3. the constant keeps the original stream
  4. setter demands a write method
  5. warn goes to $stderr

basics

~10 s

$stdout is a global variable holding the current standard output stream, and Kernel#puts writes to it; STDOUT is a constant holding the original stream. Redirect by reassigning $stdout; STDOUT stays as the way back.

solid answer

~40 s

Ruby names each standard stream twice. `$stdin`, `$stdout` and `$stderr` are **global variables** holding the *current* streams; `STDIN`, `STDOUT` and `STDERR` are **constants** holding the *original* ones, and at start-up each pair points at the same `IO` object. `Kernel#puts` and `print` write to whatever `$stdout` holds at the moment of the call, and `Kernel#warn` writes to `$stderr`, so reassigning the global redirects them. Its setter accepts any object that responds to `write` (a `File`, a `StringIO`) and raises `TypeError` otherwise. Reassigning `STDOUT` only earns an "already initialized constant" warning and redirects nothing, because `puts` never reads the constant; code that writes to `STDOUT` directly bypasses any redirection on purpose.

code

ruby · 15 lines
ruby
$stdout.equal?(STDOUT)        # => true at start-up

log = File.open("run.log", "a")
saved = $stdout
begin
  $stdout = log
  puts "goes to run.log"      # Kernel#puts reads $stdout
  STDOUT.puts "still on the terminal"
  warn "goes to $stderr"
ensure
  $stdout = saved
  log.close
end

$stdout = nil                 # TypeError: $stdout must have write method, NilClass given

go deeper

for a junior

Recall that $stdout is the current stream and STDOUT the original, that puts writes to $stdout, and that warn writes to $stderr.

for a middle

Explain that Kernel methods look the global up at call time, that the setter only demands a write method, and why reassigning STDOUT redirects nothing.

for a senior

Show where a global swap stops working: shared across threads, invisible to child processes and descriptor-level writers, and bypassed by code that writes to STDOUT directly.

for a principal

Argue for injecting output streams into components instead of relying on a process-wide global, and set a rule for when library code may write to the constants.

## Six names for three streams Every process starts with three **standard streams**: standard input (file descriptor 0), standard output (descriptor 1) and standard error (descriptor 2). Ruby wraps each in an `IO` object and exposes it under two names: | Stream | Global variable (current) | Constant (original) | Descriptor | |---|---|---|---| | standard input | `$stdin` | `STDIN` | 0 | | standard output | `$stdout` (alias `$>`) | `STDOUT` | 1 | | standard error | `$stderr` | `STDERR` | 2 | When the interpreter boots, each global and its constant refer to the **same object**: `$stdout.equal?(STDOUT)` is `true`. The difference is what happens later. A **global variable** can be rebound at any time; a **constant** is meant to be set once, and CRuby's own comment on `STDOUT` in `io.c` calls it the holder of "the original stdout". ## Who reads the globals The convenience methods in `Kernel` look the stream up **on every call**: - `Kernel#puts` is documented as equivalent to `$stdout.puts(objects)`, and `print` goes to the same place. - `Kernel#warn` hands its message to `Warning.warn`, whose default implementation writes to the current `$stderr`. When warnings are disabled (`ruby -W0` sets `$VERBOSE` to `nil`), `warn` prints nothing at all. - `Kernel#gets` reads through `ARGF`, which reads the current `$stdin` when `ARGV` holds no file names at the first read. Because the lookup happens at call time, rebinding the global is enough to redirect every later `puts`, `print` and `warn` in the process, including calls made deep inside library code. ## Rules for reassigning `$stdout` 1. **Anything with a `write` method is accepted.** The setter checks with `respond_to?`-style logic, so a `File`, a `StringIO` or your own object that implements `write` all work. `$stdout = nil` raises `TypeError` with the message "$stdout must have write method, NilClass given". The same check guards `$stderr`. 2. **It rebinds a name; it does not touch the old stream.** The previous `IO` is neither closed nor flushed, and any object that stored a reference to it keeps writing there. 3. **It is shared by every thread.** In CRuby the variable is per-Ractor, which in ordinary programs means one value for the whole process, so two threads that redirect it at the same time trample each other. 4. **It is a Ruby-level change, not a descriptor change.** File descriptor 1 still points where it did. A child process, or C code that writes straight to descriptor 1, is unaffected. Redirecting the descriptor itself is what `IO#reopen` does. The usual shape keeps the previous value in a local and restores it in `ensure`, so nested redirections unwind correctly: save `$stdout`, assign the new stream, run the code, and put the saved value back even if an exception escapes. ## Why not reassign `STDOUT` Assigning to `STDOUT` is a constant reassignment. Ruby allows it with the warning "already initialized constant STDOUT", and it changes nothing that matters: `puts` never consults the constant, so output still goes to `$stdout`. What you *have* done is lose the one name that always meant "the real terminal or pipe". Keeping the constant untouched gives it two legitimate uses: - **Restoring**: `$stdout = STDOUT` puts the process back to its original stream when there is no saved value to restore. - **Deliberate bypass**: a progress bar, an interactive prompt or a crash reporter that must reach the real output even while a caller has redirected `$stdout` can write to `STDOUT` or `STDERR` directly. That second use cuts both ways. Library code that writes to `STDOUT` instead of `$stdout` cannot be silenced or captured by its caller, which is why the convention is to write to the globals (or to an injected stream) and reserve the constants for the rare case that must escape redirection. ## Quick reference | Goal | Write this | |---|---| | Send all later `puts` output to a log file | `$stdout = File.open("run.log", "a")` | | Capture output in memory | `$stdout = StringIO.new` | | Put the original stream back | `$stdout = STDOUT`, or a saved local | | Print to the terminal even while redirected | `STDOUT.puts` | | Report a diagnostic | `warn "..."`, which lands on `$stderr` |

  • Why would library code deliberately write to STDERR rather than $stderr?
    To reach the process's real error stream even when a caller has swapped `$stderr` for a buffer, for example a crash reporter that must be seen. The cost is that callers can no longer capture or silence that output, so it should be reserved for messages that must escape redirection; ordinary diagnostics belong on `$stderr` or through `warn`.
  • Does reassigning $stdout redirect the output of a command started with system?
    No. The global swap changes which Ruby object `puts` calls; the child process inherits file descriptor 1, which still points at the original terminal or pipe. Redirecting a child needs the descriptor itself redirected, with `IO#reopen` or the spawn options; capturing subprocess output is its own topic.
  • What does $> refer to?
    `$>` is another name for the same current standard output: it shares `$stdout`'s getter and setter, so assigning either changes both. Its English-library name is `$DEFAULT_OUTPUT`. Most code writes `$stdout` because it reads more clearly.

$stdout is the forwarding address on your mail: change it and every later letter goes to the new place. STDOUT is the house's street address, which never changes, so you can always forward the mail back home.

saying these in an interview costs you the question

  • Reassigning STDOUT is how you redirect a script's output
  • Kernel#puts always writes to STDOUT whatever $stdout holds
  • Only an IO or File object can be assigned to $stdout
  • Reassigning $stdout also redirects child processes started with system
  • warn writes to standard output just like puts
open as a page

In Ruby, how do you capture what a command-line tool prints to $stdout and $stderr inside a test, and what output escapes that capture?

level: middleimportance: must knowfreq 50%

basics

~20 s

Swap $stdout and $stderr for StringIO objects, run the tool, read each buffer's string, and restore the originals in ensure. Child processes, writes to STDOUT or STDERR, and objects that cached the old stream escape the swap.

open as a page

In Ruby, why does a script run as `ruby greet.rb Alice` fail with Errno::ENOENT when it calls gets to read a reply?

level: middleimportance: should knowfreq 36%

basics

~20 s

Kernel#gets reads from ARGF, which treats every entry in ARGV as a file to read and uses $stdin only when ARGV starts out empty. With Alice in ARGV, gets tries to open a file named Alice. Call $stdin.gets instead.

open as a page

In Ruby, what is StringIO, and why does reading from a StringIO right after writing to it return an empty string?

level: middleimportance: should knowfreq 40%

basics

~20 s

StringIO, from the stringio default gem, wraps a String in IO-like methods such as puts, gets and read. Reads and writes share one position, so after a write it sits at the end; call rewind first, or read #string.

open as a page

A Ruby worker's puts lines reach the container log late and out of order with its warn lines, though the terminal looked fine; why, and how do you fix it?

level: seniorimportance: should knowfreq 38%

basics

~20 s

When standard output is a pipe or file, Ruby buffers STDOUT internally, while STDERR is always synchronous, so warn lines overtake puts lines. Set $stdout.sync = true at boot, or call $stdout.flush where output must appear.

open as a page

In Ruby, what does IO.pipe return, and why can a read on its reader block forever after the writer has sent everything?

level: seniorimportance: nice to knowfreq 18%

basics

~20 s

IO.pipe returns [reader, writer], two IO objects joined by an OS pipe. The reader sees end of file only when every copy of the write end is closed, so reader.read hangs while any writer, including one inherited by fork, stays open.

open as a page