Skip to content

Warnings go to stderr, and -w finds more of them

Level: 201 · anyone who has seen warning: scroll past and wondered who wrote it

One line: warn prints to stderr; -w sets $VERBOSE to true and makes Ruby report unused variables, redefined methods, mismatched indentation and literal conditions; Warning[:deprecated] is off until -W:deprecated turns it on; and prepending a method to Warning captures every warning in-process.

Ruby has one warning channel and two switches on it. The channel is Warning.warn: every warning Ruby itself emits and every warn "…" in your code goes through that one method, and it writes to stderr, so a program's stdout stays clean for the next program in the pipe. The first switch is $VERBOSE: false by default (warnings on, lint off), true under -w (lint on: parse-time checks for unused variables, mismatched indentation and literals in conditions, plus a run-time check for redefined methods), nil under -W0 (everything off, warn included). The second is per category: Warning[:deprecated] is false by default, so deprecation warnings — including warn …, category: :deprecated in your own code and the "literal string will be frozen" warning that chilled literals ↗ emit — are silent until -W:deprecated or Warning[:deprecated] = true; :experimental is on by default, :performance off. Because the channel is one method on a module, a test can capture warnings by prepending a module to Warning's singleton class. The program shows all of this through child processes, since a warning is stderr and stderr is not what a page's output block records.

Verified output of warnings_and_dash_w_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

1. warn writes to stderr, not stdout
   warn "careful"; puts "out"
      stdout: out
      stderr: careful
      exit status: 0
2. uplevel: adds the file, the line and the word warning
   warn "old api", uplevel: 0
      stderr: -e:1: warning: old api
      exit status: 0
3. Warning[:deprecated] is off by default; -W:deprecated turns it on
   ruby -e '...'
      stdout: Warning[:deprecated] = false
      exit status: 0
   ruby -W:deprecated -e '...'
      stdout: Warning[:deprecated] = true
      stderr: old
      exit status: 0
4. $VERBOSE is false by default, true under -w, nil under -W0 (which silences warn)
   ruby -e 'p $VERBOSE; warn "shown?"'
      stdout: false
      stderr: shown?
      exit status: 0
   ruby -w -e '...'
      stdout: true
      stderr: shown?
      exit status: 0
   ruby -W0 -e '...'
      stdout: nil
      exit status: 0
5. the warning categories and their defaults
   Warning.categories [:deprecated, :experimental, :performance, :strict_unused_block]
   Warning[:deprecated]             false
   Warning[:experimental]           true
   Warning[:performance]            false
   Warning[:strict_unused_block]    false
6. what -w reports on a sloppy file, and -c, the syntax check
   ruby sloppy.rb
      stdout: literal condition
      stdout: hi again
      exit status: 0
   ruby -w sloppy.rb
      stdout: literal condition
      stdout: hi again
      stderr: sloppy.rb:2: warning: assigned but unused variable - unused
      stderr: sloppy.rb:10: warning: literal in condition
      stderr: sloppy.rb:16: warning: mismatched indentations at 'end' with 'def' at 14
      stderr: sloppy.rb:15: warning: assigned but unused variable - x
      stderr: sloppy.rb:6: warning: method redefined; discarding old greet
      stderr: sloppy.rb:1: warning: previous definition of greet was here
      exit status: 0
   ruby -c sloppy.rb
      stdout: Syntax OK
      exit status: 0
7. capturing warnings in-process by prepending to Warning
   captured [["captured one", nil], ["captured three", :deprecated]]
   (the second never reached Warning.warn: its category was off at the time)
8. mutating a string literal warns only under -W:deprecated (literals are chilled)
   ruby -e 's = "lit"; s << "x"; puts s'
      stdout: litx
      exit status: 0
   ruby -W:deprecated -e '...'
      stdout: litx
      stderr: -e:1: warning: literal string will be frozen in the future (run with --debug-frozen-string-literal for more information)
      exit status: 0

Reading the output

Row 1 is the channel: puts lands in the child's stdout, warn in its stderr. Row 2 adds uplevel: 0, and the warning gains the -e:1: warning: prefix that Ruby's own warnings carry. Row 3 is the category switch: by default warn "old", category: :deprecated prints nothing and Warning[:deprecated] is false; under -W:deprecated both change. Row 4 is $VERBOSE under the three settings, and shows that -W0 silences even a plain warn. Row 5 lists the four categories Ruby 4.0 knows and their defaults: only :experimental is on.

Row 6 runs one sloppy file three ways. Plain ruby runs it silently. ruby -w reports six lines: the parse-time warnings first (an unused variable on line 2, a literal in a condition on line 10, the end on line 16 that does not line up with its def on line 14, another unused variable on line 15), then, once execution reaches the second def greet, the run-time pair "method redefined; discarding old greet" and "previous definition of greet was here". ruby -c parses without running and prints Syntax OK — a syntax check, not a lint; none of the six warnings are errors. Row 7 captures in-process: after the prepend, warn "captured one" reaches our method with category nil; warn "captured two", category: :deprecated never reaches it, because a disabled category is filtered before Warning.warn is called; after Warning[:deprecated] = true, the third one arrives with its category. Row 8 is the chilled-literal warning: s << "x" on a literal succeeds silently by default and prints "literal string will be frozen in the future" under -W:deprecated.

Compared with Python

Python's warnings.warn also writes to stderr (row 1), but it always prefixes file:line: Category: — and stacklevel=2 moves the blame from the warn call to its caller, which is what uplevel: does in Ruby (row 2). The switches are per category and per module rather than a global $VERBOSE. Row 3 is the one that surprises: a DeprecationWarning is shown when it is raised in __main__ and hidden when the same call sits in an imported module — the default filter is meant to nag the developer of the code, not its user — and -W default or -X dev shows it everywhere. Row 4: -W error turns the warning into an exception (the child exits 1 with UserWarning: careful as the last line of a traceback), -W ignore drops it. Row 5 asks a child for each category and reads off the defaults: PendingDeprecationWarning, ImportWarning and ResourceWarning are hidden even from __main__. Row 6: there is no -w lint pass; the closest thing is the compiler's SyntaxWarning for constructs it can tell are wrong, such as is with a literal or an assert on a tuple. Row 7 is the in-process capture, built in as warnings.catch_warnings(record=True) — note the simplefilter("always") so the DeprecationWarning is recorded too. Row 8 is the structural difference under all of it: Warning is a subclass of Exception, which is why a warning can become an error at all, where Ruby's Warning is a module with a method. Dev mode also turns on ResourceWarning and extra runtime checks; the twin prints only sys.flags.dev_mode and sys.warnoptions. (Not machine-checked here.)

Verified output of warnings_and_dash_w_py.py — regenerated by tools/run_examples.py, never hand-typed.

1. warnings.warn writes to stderr, not stdout
   warnings.warn('careful'); print('out')
      stdout: out
      stderr: <string>:1: UserWarning: careful
      exit status: 0
2. the prefix is always file:line: Category; stacklevel=2 blames the caller
   warn from inside old(), default stacklevel
      stderr: <string>:3: UserWarning: old api
      exit status: 0
   warn from inside old(), stacklevel=2
      stderr: <string>:4: UserWarning: old api
      exit status: 0
3. DeprecationWarning is shown from __main__ only; -W default and -X dev show it everywhere
   python3 -c 'warnings.warn("old", DeprecationWarning)'
      stdout: out
      stderr: <string>:1: DeprecationWarning: old
      exit status: 0
   the same warning raised inside an imported module
      stdout: out
      exit status: 0
   ... under python3 -W default
      stdout: out
      stderr: oldlib.py:5: DeprecationWarning: old api
      exit status: 0
   ... under python3 -X dev
      stdout: out
      stderr: oldlib.py:5: DeprecationWarning: old api
      exit status: 0
4. -W error turns a warning into an exception; -W ignore silences it
   python3 -W error -c '...'
      stderr: UserWarning: careful
      exit status: 1
   python3 -W ignore -c '...'
      stdout: out
      exit status: 0
5. the categories, and whether a warning of each is shown from __main__ by default
   UserWarning                shown
   DeprecationWarning         shown
   PendingDeprecationWarning  hidden
   SyntaxWarning              shown
   RuntimeWarning             shown
   FutureWarning              shown
   ImportWarning              hidden
   ResourceWarning            hidden
6. no -w: the closest thing is the compiler's SyntaxWarning
   if 'a' is 'a': ...
      stdout: compared
      stderr: <string>:1: SyntaxWarning: "is" with 'str' literal. Did you mean "=="?
      exit status: 0
   assert (1, 'msg')
      stderr: <string>:1: SyntaxWarning: assertion is always true, perhaps remove parentheses?
      exit status: 0
7. capturing warnings in-process with catch_warnings(record=True)
   captured [('captured one', 'UserWarning'), ('captured two', 'DeprecationWarning')]
8. a Warning is an Exception, which is what makes -W error possible
   DeprecationWarning.__mro__ ['DeprecationWarning', 'Warning', 'Exception', 'BaseException', 'object']
   python3 -X dev: sys.flags.dev_mode
      stdout: True ['default']
      exit status: 0
   default: sys.flags.dev_mode
      stdout: False []
      exit status: 0

The Python reference for the filters and the flags is the warnings module ↗; the Python library's -c is not the prompt ↗ explains the -c children this twin runs.

Try it

  1. Write a file whose total method assigns a variable it never uses and is then defined a second time. Run it with Open3.capture3(RbConfig.ruby, path) and again with -w before the path; print each run's stdout, status and stderr with the temp-dir path cut to the base name, and read off which run reported the two problems.
  2. Extend row 7 into a capture_warnings { … } helper that prepends the capturing module, yields, returns the collected [message, category] pairs, and restores the original behaviour afterwards (a flag the prepended method checks is enough). Show that a warn after the block reaches stderr again.
Solution to 1

warnings_and_dash_w_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Exercise 1: write a sloppy file, run it under ruby -w and without, and print what
# each run said on stderr with the temp-dir path cut to the base name.

require "open3"
require "rbconfig"
require "tmpdir"

SLOPPY = <<~RUBY
  def total(items)
    count = items.size
    items.sum
  end

  def total(items)
    items.sum
  end

  puts total([1, 2, 3])
RUBY

Dir.mktmpdir do |dir|
  path = File.join(dir, "sloppy.rb")
  File.write(path, SLOPPY)
  [["without -w"], ["with -w", "-w"]].each do |label, *flags|
    out, err, status = Open3.capture3(RbConfig.ruby, *flags, path)
    puts "#{label}: stdout #{out.chomp.inspect}, status #{status.exitstatus}"
    if err.empty?
      puts "   stderr: (empty)"
    else
      err.each_line { |line| puts "   stderr: #{line.chomp.sub(%r{\A\S*/}, '')}" }
    end
  end
end

Verified output of warnings_and_dash_w_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

without -w: stdout "6", status 0
   stderr: (empty)
with -w: stdout "6", status 0
   stderr: sloppy.rb:2: warning: assigned but unused variable - count
   stderr: sloppy.rb:6: warning: method redefined; discarding old total
   stderr: sloppy.rb:1: warning: previous definition of total was here

See also