Hash value omission - an introduction and some examples

The hash literal value omission was added to Ruby version 3.1 in 2021. This is the feature proposal that describes the feature request and the one that was implemented.
What does it mean?
Hash literal value omission (or hash value omission) means that { name: } is a shortcut for { name: name }
url = 'https://shortruby.com'
source = 'email'
link = { url:, source: }
# is a shortcut for
link = { url: url, source: source }
This works also for method calls with keyword arguments:
url = "https://ruby.social/@lucian"
result = UrlType.detect(url:)
# this is equivalent to writing
result = UrlType.detect(url: url)
And of course you can use it to pass values via keyword arguments to other methods when calling from inside current method:
def get(path:)
request(:get, path:)
end
It also searches through the methods inside the current object:
class MetaTags
def initialize(article:)
@article = article
end
def to_meta_tags
{ title:, description:, keywords: }.compact_blank
end
private
def title = "#{article.title} - #{CurrentSection.title}"
def description = article.description
def keywords = article.tags + [CurrentSection.keyword]
end
Notice there inside the method to_meta_tags there are no local variables named title, description or keywords, but there are private methods with that name. So when composing the hash, Ruby will look not only at local variables, but also try to find them among the methods of the current object.
What you see here is, in my opinion, the most powerful use of hash literal omission when combined with endless methods: a concise way to create a hash with some computed attributes while keeping the methods very short (one line).
How it works?
The following code is an approximation (like a pseudocode) of how it works. I discovered this simple explanation by Akinori Musha in the feature request:
# This is not the real code, but a possible explanation of
# how it works if it would be implemented in Ruby
# Source: https://bugs.ruby-lang.org/issues/14579#note-32
value = if binding.local_variable_defined?(:user)
binding.local_variable_get(:user)
else
__send__(:user)
end
{ user: value }
It also has a nice explanatory error message that describes in a way how it works.
Let's write a simple example inside a file called error_message.rb.
# error_message.rb
my_hash = { name: }
When executed with ruby error_message.rb, it will display the following error message:
error_message.rb:1:in `<main>': undefined local variable or method `name' for main (NameError)
my_hash = { name: }
^^^^^
The error message is explicit:
- it says "undefined local variable or method", hinting on the fact that it tries to get the value for
nameeither from a local variable or a method calledname.
Why use this feature?
Before going into my own explanation I think this short reply from Victor Shepelev in Reddit makes a good compelling case for this feature:

I will touch some of these points below, but I think Victor is doing here a great job explaining in a coincise way why this feature is not hard to understand or to use.
Removes repetition
I think this kind of code can be easily found in multiple projects:
user = User.find(params[:id]) # or some other way to find a user
authenticate(user: user, context: context)
# or
def get(path, query_params: {})
request(:get, path: path, query_params: query_params)
end
# or
render partial: "user", locals: { user: user, organisation: organisation }
And with hash value omission it can be transformed into:
user = User.find(params[:id]) # or some other way to find a user
authenticate(user:, context:)
# or
def get(path, query_params: {})
request(:get, path:, query_params:)
end
# or
render partial: "user", locals: { user:, organisation:}
A more general example
The following idiom is pretty common in a Rails application that uses keyword arguments:
method(foo: foo, baz: baz, qux: qux)
It can easily go into a longer line:
media_converter.create_job(role: role, settings: settings, queue: queue, project: project, input: input)
or this expression can be split into multiple lines:
media_converter.create_job(
role: role,
settings: settings,
queue: queue,
project: project,
input: input
)
There might be two issues with this type of code:
Repeating the same label multiple times
This makes the line longer and sometimes requires splitting it into multiple lines. A shorter version could help keep the code on a single line.
None of these indicate poor code quality. This feature is more about making things a bit better rather than fixing bad code.
In both cases, the code can be written in a more concise form:
method(foo:, baz:, qux:)
media_converter.create_job(role:, settings:, queue:, project:, input:)
Brings symmetry to pattern matching
When reading the feature request Matz decides to accept the proposal with the following argument:
After the RubyKaigi 2021 sessions, we have discussed this issue and I was finally persuaded.
Our mindset has been updated (mostly due to mandatory keyword arguments).
Accepted.Matz.
It is not that clear from reading the notes there what was the other argument that convincend him along with keyword arguments.
Looking at Dev Meeting Log the following was noted:
matz: Yes, they should. I was convinced by the points knu made.
Thus I asked Akinori Musha via Ruby.social about what were the arguments and he shared the following:


In this case hash value omission is seen as a composition, while pattern matching is using decomposition. I think it is an elegant way to think about this feature.
He also mentioned that the actual example he used in that discussion was one shared also in the Ruby issue tracker where you can see the same idea of composition and decomposition in a single method:
def get_user_profile(client)
client.get_json("/current_user") => { id: }
client.get_json("/profile", { id: }) => { nick:, bio: }
return { id:, nick:, bio: }
end
It is a known syntax
I know that the main argument against this syntax is that is is weird. And I read this weirdness more in the direction of not being familiar. But you can see this syntax in at least two places:
Keyword arguments
Pattern matching
Here is an example from keyword arguments:

And here is an example from pattern matching (but in pattern matching this kind of syntax is common):

A simple Rails controller
Let's take a look how this can be used in a Rails controller. Assume we have a controller that should create an RSS feed something like this:
class Blog::FeedController < ApplicationController
def index
posts = RSS::Posts.new.ordered
renderer = ApplicationMarkdown.new.renderer
respond_to do |format|
format.xml { render locals: { posts: posts, renderer: renderer } }
end
end
end
You can notice the repetition happening at:
{ posts: posts, renderer: renderer }
Let's do a couple of changes to this code:
- Removing hash values:
class Blog::FeedController < ApplicationController
def index
posts = RSS::Posts.new.ordered
renderer = ApplicationMarkdown.new.renderer
respond_to do |format|
- format.xml { render locals: { posts: posts, renderer: renderer } }
+ format.xml { render locals: { posts: , renderer: } }
end
end
end
We could stop at this change, but I think we can make it a bit more concise or at least play a bit more with the advantage that hash value omission allows the use of methods.
- Move
postsandrenderedto their one methods
I am using here the endless method but it will work the same with normal method:
class Blog::FeedController < ApplicationController
def index
- posts = RSS::Posts.new.ordered
- renderer = ApplicationMarkdown.new.renderer
respond_to do |format|
format.xml { render locals: { posts: , renderer: } }
end
end
private
+ def posts = RSS::Posts.new.ordered
+ def renderer = ApplicationMarkdown.new.renderer
end
Now it will look like this:
class Blog::FeedController < ApplicationController
def index
respond_to do |format|
format.xml { render locals: { posts: , renderer: } }
end
end
private
def posts = RSS::Posts.new.ordered
def renderer = ApplicationMarkdown.new.renderer
end
That for me it is concise enough and has the avdantage that the index action is only concerned with the response and composing what is needed for the response while the posts and rendereder method are creating the necessary objects outside the action because those objects does not depend on the request/response
- Push it a bit more
The following change is push the changes a bit too far by extracting also locals in a separate method.
class Blog::FeedController < ApplicationController
def index
respond_to do |format|
format.xml { render locals: }
end
end
private
def locals = { posts: , renderer: }
def posts = RSS::Posts.new.ordered
def renderer = ApplicationMarkdown.new.renderer
end
This can have the advantage that it isolates the purpose of the action method even more about composing the response while delegating the actual composition to the locals method. Insofar that if I want to add one more variable to be passed to the response, I don't need any change in the action (which remains focused on composing the response without knowing too much about the internals) and I need only to change the locals method.
But it has the disadvantage that is hides those variables in the method so when reading the action index it is not clear in a quick glance what will be passed on to the response.
Guidelines
Do not mix hash value omission with providing explicit values
I agree here with Rubocop rule that either omit value for all hash keys or provide explicit values but don't mix them. It makes reading the code easier for a lot of people. I personally don't have a big issue with reading also mixed style, but I heared a lot of people complain about it.
# Do not do this
{ posts:, renderer: renderer }
# Do this
{ posts: posts, renderer: renderer }
# Or do this
{ posts:, renderer: }
Always use paranthesis
There are some edge cases where using the hash value omission might create undesired effects.
Let's assume there are two methods:
def bar(x)
x * 2
end
def foo(a:, b:)
puts "#{a} and #{b}"
end
And now let's test what happens when we try to call foo with hash value omission and then run bar after it.
a = 1
b = 2
puts "Call with parantheses"
foo(a:, b:)
bar(3)
The output of this will be:
Call with parantheses
1 and 2
But what if we try to make the calls without paranthesis:
a = 1
b = 2
puts "Call without parantheses"
foo a:, b:
bar(3)
Can you guess what will be the output?
It will be:
Call without parantheses
1 and 6
This happens because it will execute b: bar(b) as in:
# The following code
foo a:, b:
bar(b)
# Is interpreted as being:
foo(a:, b: bar(3))
Conclusion: Always use paranthesis with hash value omission
An extra trick
The following trick was shared by Shugo Maeda, the author of the proposal, on the request feature.
Because of how it works you can replace the code that looks like this:
def button(class:)
binding.local_variable_get(:class)
end
With code that looks like this:
def button(class:)
{ class: }[:class]
end
And it is faster too. This benchmark:
def access_via_binding(class:)
binding.local_variable_get(:class)
end
def access_via_hash_value_omission(class:)
{ class: }[:class]
end
Benchmark.ips do |x|
x.report("method: binding.local_variable_get") do
access_via_binding(class: :sm)
end
x.report("method: hash literal omission") do
access_via_hash_value_omission(class: :sm)
end
x.compare!
end
will output the following results on my machine (Macbook M3 Pro):
ruby 3.3.0 (2023-12-25 revision 5124f9ac75) [arm64-darwin23]
Warming up --------------------------------------
method: binding.local_variable_get
532.350k i/100ms
method: hash literal omission
1.580M i/100ms
Calculating -------------------------------------
method: binding.local_variable_get
5.371M (± 0.6%) i/s - 27.150M in 5.054806s
method: hash literal omission
15.776M (± 0.6%) i/s - 79.012M in 5.008478s
Comparison:
method: hash literal omission: 15776220.8 i/s
method: binding.local_variable_get: 5371270.0 i/s - 2.94x slower
Other resources
If you want to read more about this feature I recommend:
The release notes of Ruby 3.1: https://www.ruby-lang.org/en/news/2021/12/25/ruby-3-1-0-released/
The Ruby Change log entry about this feature: https://rubyreferences.github.io/rubychanges/3.1.html#values-in-hash-literals-and-keyword-arguments-can-be-omitted
A deep dive by Victor Shepelev https://zverok.space/blog/2023-11-10-syntax-sugar3-hash-values-omission.html
A critique from Bozhidar Batsov https://batsov.com/articles/2022/01/20/bad-ruby-hash-value-omission
A proposal to adop hash value omission I wrote when I was working at Cookpad to merge it to our style guides: https://github.com/cookpad/global-style-guides/pull/195
Enjoyed this article?
👐 Subscribe to my Ruby and Ruby on Rails courses over email at learn.shortruby.com- effortless learning about Ruby anytime, anywhere
👉 Join my Short Ruby Newsletter for weekly Ruby updates from the community and visit rubyandrails.info, a directory with learning content about Ruby.
🤝 Let's connect on Ruby.social or Linkedin or Twitter where I post mainly about Ruby and Ruby on Rails
🎥 Follow me on my YouTube channel for short videos about Ruby




