Posted 4 months ago. Visible to the public. Repeats. Linked content.

Changes to positional and keyword args in Ruby 3.0

Ruby 3.0 introduced a breaking change in how it treats keyword arguments.

There is an excellent blog post on the official Ruby blog going into the details. You should probably read that, but here is a slightly abbreviated version:

What changed

If you call a method that accepts keyword arguments, either explicitly or via **args, you cannot call it with a hash. I.e.

def explicit_kwargs(x:, y:) # ... end def splat_kwargs(**kwargs) # ... end def no_kwargs(*args) # ... end explicit_kwargs(x: 1, y: 2) # works explicit_kwargs({x: 1, y: 2}) # raises an ArgumentError explicit_kwargs(**{x: 1, y: 2}) # works splat_kwargs(x: 1, y: 2) # works splat_kwargs({x: 1, y: 2}) # raises an ArgumentError splat_kwargs(**{x: 1, y: 2}) # works no_kwargs(x: 1, y: 2) # works, args = [{x: 1, y: 2}] no_kwargs({x: 1, y: 2}) # also works!

Why it was changed

It was felt that the automatic conversion lead to too many unexpected edge cases and bugs, such as this one.

What will break

The change will be felt most in code that is supposed to work across Ruby 2 and 3, especially when using delegation.

In the olden times, Ruby delegation would be done like this:

def wrapper(*args, &block) # do something wrapped_method(*args, &block) end

In Ruby 3, this will only work if wrapped_method happens to not accept keyword arguments. If it does, this will break. The correct way to do delegation in Ruby 3 is

def wrapper(*args, **kwargs, &block) # do something wrapped_method(*args, **kwargs, &block) end

However, this style will not work correctly in older Rubies. There are differences between Ruby 2.7, and 2.6 and lower, but the effect is that the wrapped_method may end up with an extra or missing empty hash.

To achieve delegation across Ruby versions, if the target method (might) accept keyword args, the recommended way is

ruby2_keywords def wrapper(*args, &block) # You cannot accept **kwargs of any kind here! # do something wrapped_method(*args, &block) end

ruby2_keywords(method_name) was introduced in 2.7. It does some stupidly complex stuff, but basically makes things just work.

In Ruby 2.6 and below you can add the ruby2_keywords-Gem, which will polyfill the method with a noop (since in Ruby 2.6, this style of delegation always works fine anyways).

The "forward everything" syntax

Starting with Ruby 2.7 there is also the following syntax to "forward" everything. I don't consider it very useful, since it is not available on older Rubies, and it prevents you from doing anything with the arguments before forwarding, but it works like this:

def wrapper(...) wrapped_method(...) end
Growing Rails Applications in Practice
Check out our new e-book:
Learn to structure large Ruby on Rails codebases with the tools you already know and love.

Owner of this card:

Tobias Kraze
Last edit:
4 months ago
by Tobias Kraze
About this deck:
We are makandra and do test-driven, agile Ruby on Rails software development.
License for source code
Posted by Tobias Kraze to makandra dev
This website uses short-lived cookies to improve usability.
Accept or learn more