Ruby 2.7 deprecates automatic conversion from a hash to keyword arguments


The method definition in Ruby is extremely flexible. The example from Marc-André Lafortune blog sums it up nicely.

class C
  def hi(needed, needed2,
         maybe1 = "42", maybe2 = maybe1.upcase,
         *args,
         named1: 'hello', named2: a_method(named1, needed2),
         **options,
         &block)
  end
end

C.instance_method(:hi).parameters
# => [ [:req, :needed], [:req, :needed2],
#      [:opt, :maybe1], [:opt, :maybe2],
#      [:rest, :args],
#      [:key, :named1], [:key, :named2],
#      [:keyrest, :options],
#      [:block, :block] ]

Ruby 2.7 will bring in certain changes to the keyword arguments design, but first, let’s understand what are keyword arguments?

What are keyword arguments?

Keyword arguments were introduced in Ruby 2 and treated just like optional arguments ( hash object ). We can pass an optional argument when the function accepts keyword arguments and vice versa.

def a_method(k: 1)
  puts "k: #{k}"
end

a_method
=> k: 1
a_method(k: 2)
=> k: 2
a_method({k: 2})
=> k: 2

def a_method(**kw)
  puts "kw, #{kw}"
end

a_method
=> kw, {}
a_method(k: 2)
=> kw, {:k=>2}
a_method({k: 2})
=> kw, {:k=>2}

The compatibility between keyword arguments and optional arguments have been a source of a number of bugs and edge cases as pointed out in the feature description of the “Real” keyword argument

In RubyConf 2017, Matz had officially announced that Ruby 3.0 will have “real” keyword arguments i.e a keyword argument will be completely separated from normal arguments.

As the change will be incompatible, automatic conversion from a Hash to keyword arguments has been deprecated in Ruby 2.7 to prepare for the redesign of keyword arguments in Ruby 3.0.

Here is how it might affect the code in use, and how we can migrate it for Ruby 3.0 support.

When the method accept keywords and a Hash is passed

def a_method(k: 1)
  puts "k: #{k}"
end

a_method({k: 1})
(irb):4: warning: The last argument is used as the keyword parameter
(irb):1: warning: for `a_method' defined here
=> k: 1

# To avoid the warning and make it Ruby 3 compatible,
# use the double splat operator

a_method(**{k: 1})
=> k: 1

When the method accepts both optional and keyword arguments, but not enough arguments are passed

def a_method(opts, **kw)
  puts "opts, #{opts}"
  puts "kw, #{kw}"
end

a_method(k: 1)
(irb):5: warning: The keyword argument is passed as the last hash parameter
(irb):1: warning: for `a_method' defined here
=> opts, {:k=>1}
kw, {}

# To avoid the warning and make it Ruby 3 compatible,
# pass Hash object as the argument

a_method({k: 1})
=> opts, {:k=>1}
=> kw, {}

# We can explicitly mark that the method accepts no keywords using ```**nil```
def a_method(opts, **nil)
  puts "opts, #{opts}"
end

a_method(k: 1)
=> ArgumentError (no keywords accepted)

Compatibility layer

As a compatibility layer, passing keyword arguments to a method that accepts optional arguments is allowed.

def a_method(opts={})
  puts "opts, #{opts}"
end

a_method(k: 1)
=> opts, {:k=>1}

Non-symbols allowed as keyword argument keys

When a method accepts arbitrary keywords ( using the double splat operator ), non-symbols are allowed as keyword argument keys.

Ruby 2.6

def a_method(**kw)
  puts "kw, #{kw}"
end

a_method("k" => 1)
ArgumentError: wrong number of arguments (given 1, expected 0)
from (pry):7:in `a_method'

a_method(**{"k" => 1})
TypeError: wrong argument type String (expected Symbol)
from (pry):11:in `__pry__'

Ruby 2.7

def a_method(**kw)
  puts "kw, #{kw}"
end

a_method("k" => 1)
=> kw, {"k"=>1}

Summary

Ruby 2.7 has deprecated automatic conversion from a hash to keyword arguments. As seen from various scenarios, we can start looking at migrating our applications over deprecated usage. There is ongoing development around this area, and we might see more changes related to this land in upcoming Ruby versions.