October 2, 2020·5 min read·engineeringrubycompatibilitykeywords

Ruby keyword migration warnings become optional

Ruby 2.7.2 disables deprecation warnings by default while retaining argument conversions scheduled for removal in Ruby 3.

Ruby 2.7 has been asking applications to prepare for a change that their current interpreter still accommodates. A trailing positional hash can become keyword arguments, but the interpreter warns that this conversion is deprecated. In a dependency-heavy application, the person reading that warning may have no direct control over the method producing it. The discussion in Ruby issue 17000 records the disagreement about how much migration work an interpreter upgrade should impose.

The Ruby 2.7.2 announcement changes the default on October 2. Deprecation warnings are disabled unless requested. That includes keyword migration diagnostics, but the switch covers the deprecation category as a whole. Programs retain the transitional argument behavior of Ruby 2.7. A quiet run consequently says less about readiness for Ruby 3 than it did before this patch.

I value Ruby's readable method calls, especially when named options explain an operation without a separate configuration object. The difficulty here is that similar-looking calls have acquired different meanings. Suppressing a diagnostic makes ordinary execution less distracting, but it cannot settle which meaning a library intended.

A hash and keyword arguments

The underlying change separates positional arguments from keyword arguments. Ruby's migration explanation describes the planned Ruby 3 behavior and the transitional warnings. Consider a method with one keyword parameter and a hash held in a variable.

def render(limit: 10)
  limit
end

options = { limit: 25 }
render(options)
render(**options)

On Ruby 2.7, the first call converts the positional hash into keywords. With deprecation warnings enabled, it reports that conversion. The second call explicitly supplies keywords. Ruby 3 is scheduled to reject the first form for this method. Ruby 2.7.2 does not bring that rejection forward or restore a promise that the conversion will remain supported.

The method definition matters as much as the call. A method accepting a positional options hash, without keyword parameters, has a different contract. Calls using keyword-style syntax can still supply that hash. Adding a double splat to every final hash is therefore not a mechanical migration rule. It expresses an argument category, and the receiving method determines whether that category is appropriate.

The 2.7.2 keyword argument tests make these distinctions concrete. They exercise methods accepting only keywords, methods accepting a required positional argument plus keywords, and methods with an optional positional argument. Some conversions produce a warning and a successful result. Others produce a warning before an argument error. Diagnostic output and successful invocation are separate observations in these tests.

Empty hashes expose another difficulty. An empty set of keywords supplies no keyword values, while an empty positional hash supplies one positional object. The test suite checks both m(**{}) and calls passing a hash directly. Treating these forms as interchangeable loses information about the number and category of arguments. That information becomes especially important when a wrapper forwards a call to another method.

The implementation changes a category flag

The relevant interpreter implementation stores disabled warning categories in a bit mask. Ruby 2.7.2 initializes that mask with the deprecated category disabled. Category lookup recognizes deprecated and experimental, and Warning[category] reports whether the corresponding category is enabled. This changes warning policy inside the interpreter without rewriting deprecated call sites.

A process can request deprecation warnings at startup with the category-specific command-line option documented in the release announcement. That choice applies before the application begins executing.

ruby -W:deprecated app.rb

The announcement also gives -w, which enables verbose warnings. The category-specific option is more precise when the objective is to inspect deprecated behavior. Ruby code can change the same category flag directly.

Warning[:deprecated] = true

That assignment needs to happen before the operations whose diagnostics matter. Enabling it after loading a library cannot recover warnings from code that has already executed. A warning-controlled run observes the paths that actually execute, so even an enabled category does not establish compatibility for uncalled methods.

The source documents other members of the deprecated category, including creating a proc or lambda without an explicit block and certain assignments to Ruby's global separator variables. Calling the release a keyword-warning toggle understates its effect. Existing consumers of those other warnings also inherit the new default. Experimental warnings belong to a separate category and do not follow from the deprecated bit being disabled.

The implementation also exposes Warning.warn, whose default behavior writes to standard error. Its documentation permits overriding that method to filter or redirect warnings. This acts at a different stage from disabling a category. Category control decides whether a category-specific warning is emitted at all. A downstream handler cannot inspect a message the interpreter has already suppressed. That distinction matters to tools attempting to collect warnings without printing every message to an application's normal log.

I prefer the explicit category option for migration work because it states what the execution is examining. Unrelated verbose diagnostics make it harder to distinguish obsolete argument forwarding from other warnings. The interpreter already has a category mechanism, and this release makes opting into it consequential.

Delegation preserves the ambiguity

A wrapper accepting *args and a block can conceal the original caller's intent. The eventual receiver may require keywords even though the wrapper represents incoming arguments as a positional array. Ruby's migration document discusses explicit **kwargs forwarding and the ruby2_keywords compatibility mechanism for libraries spanning interpreter versions. These change the forwarding contract as well as the final call's punctuation.

For code targeting Ruby 2.7 and the planned Ruby 3 behavior, a wrapper can keep argument categories explicit. Its parameter list needs to represent both categories before forwarding them to the destination.

def relay(*args, **kwargs, &block)
  destination(*args, **kwargs, &block)
end

The wrapper now forwards positional arguments separately from keywords. Supporting older Rubies changes the available choices, which is why the compatibility mechanism exists. A library author cannot infer the right rewrite solely from a warning line at the eventual receiver. Intermediate methods and supported interpreter versions are part of that decision.

The warning debate concerns who carries migration costs during the remaining Ruby 2.7 period. Default diagnostics reach application owners early, including owners unable to update a dependency immediately. Optional diagnostics allow quieter operation but require an explicit decision to inspect deprecated behavior. The issue discussion contains objections to both outcomes. Disabling the whole category also avoids introducing a separate keyword category in this patch release.

Ruby 2.7.2 includes an updated WEBrick security fix as well, according to its announcement. Accepting that patch and continuing keyword migration are compatible decisions. I am skeptical of treating silence after an interpreter update as evidence that the language transition has become simpler. The default warning flag changed while the distinctions between positional hashes, keywords, and forwarding methods remain present in the interpreter and its tests.