One Scope Issue with Ruby 2 and 3 Keyword Argument Updates

January 10, 2023

The other day we had an issue with scope when migrating from the Ruby 2 to Ruby 3 syntax for hash arguments. Previously we had something like:

def foo(**kwargs)
  kwargs[:value] += 1
end

In Ruby 2 you can then do:

hash = { value: 1 }
foo(hash)

and the value of kwargs[:value] inside the method would increment, but it would not impact the value outside the method.

In Ruby 3 foo(hash) is now an error:

(irb):1:in `foo': wrong number of arguments (given 1, expected 0) (ArgumentError)

To fix this the migration guide suggests you might change the method to read:

def foo(kwargs = {})
  kwargs[:value] += 1
end

This is mostly equivalent but the trick is this now mutates the value of kwargs[:value] outside the method. Suddenly you might see bugs you did not see before.

What This Means

In Ruby 2 def foo(**kwargs) creates a copy of the hash and changes it inside the method without the impacting its value outside the method. In Ruby 2 and 3 the def foo(kwargs = {}) syntax merely passes the hash into the method and any changes you make to it are seen inside or outside the method.

Want to get posts like this in your email?

This work by Matt Zagaja is licensed under a Creative Commons Attribution-NonCommercial-ShareAlike 3.0 Unported License.