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¶
- Write a file whose
totalmethod assigns a variable it never uses and is then defined a second time. Run it withOpen3.capture3(RbConfig.ruby, path)and again with-wbefore 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. - 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 awarnafter 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.
See also¶
- The command-line flags —
-w,-W0,-W:no-deprecatedand-camong the rest - Classes are open — the "method redefined" warning of row 6, from the side that causes it
- Style and naming — the mismatched-indentation warning as a style check
exitraisesSystemExit— the other lesson here that reads children's stderr and exit status- String literals are chilled, not frozen ↗ — the Ruby text library on the warning in row 8
- Warning ↗ — the reference:
Warning[],Warning.warn,categories - Parentheses are optional, but the space is not — Ruby lets you leave the parentheses off a call and off a
def, so a bare name is a call - Constants are found lexically, then through ancestors — A bare constant name is resolved by the lexical nesting the code was written in (
Module.nesting), then… putsadds a newline,printdoes not,pshowsinspect—putswritesto_splus a newline it does not double, flattening arrays, printingnilas an empty line…