Hashes

A hash is a collection of key/value pairs. You hand Ruby a key and get the matching value back. Unlike arrays (which are indexed by position), hashes are indexed by whatever you want — a string, a symbol, a number, even an object.

$ irb
>> prices = { 'egg' => 0.1, 'butter' => 0.99 }
=> {"egg"=>0.1, "butter"=>0.99}
>> prices['egg']
=> 0.1
>> prices.count
=> 2
>> exit

Hash values can be anything — strings, numbers, arrays, other hashes, or instances of your own classes.

Symbols as Keys

In practice you’ll usually see symbols used as hash keys rather than strings. They’re cheaper, they compare faster, and Ruby has a dedicated shorthand for them:

$ irb
>> colors = { black: '#000000', white: '#FFFFFF' }
=> {:black=>"#000000", :white=>"#FFFFFF"}
>> colors[:white]
=> "#FFFFFF"
>> exit

{ black: '#000000' } and { :black ⇒ '#000000' } mean the same thing. The short form only works when the key is a symbol.

Agentic Coding Tip: Symbol vs String Keys — Pick One Per Hash

A hash indexed by "color" is a different hash from one indexed by :color. Mixing the two in the same structure is one of the most common silent bugs in Ruby code:

$ irb
>> colors = { "black" => "#000000" }
=> {"black"=>"#000000"}
>> colors[:black]
=> nil
>> colors["black"]
=> "#000000"
>> exit

When Claude stitches code together from multiple examples it will sometimes produce a mix — a config hash built with string keys, read with symbols in one helper, and with strings in another. Everything works in the test case the agent wrote. It fails in the one it didn’t.

Rule to add to your project’s CLAUDE.md:

For every hash I maintain, pick one key type (symbols or
strings) and stay consistent throughout. Default to symbols
for internal hashes: `{ color: :red }`, not
`{ "color" => :red }`. Strings are appropriate for hashes
that model external data (JSON payloads, YAML config, HTTP
params) where the keys already arrive as strings. Never
write code that silently assumes both forms work on the
same hash.

Inside a Rails project the params hash is special — it uses a HashWithIndifferentAccess that accepts both params[:id] and params["id"]. That special-case is the reason the mix-up is so easy: readers (and agents) assume it generalises to every hash. It doesn’t. Plain Ruby hashes treat :black and "black" as different keys, always.

Iterator each

each walks the hash and yields two values per pair: the key and the value.

$ irb
>> colors = { black: '#000000', white: '#FFFFFF' }
=> {:black=>"#000000", :white=>"#FFFFFF"}
>> colors.each do |key, value|
?>   puts "#{key} #{value}"
>> end
black #000000
white #FFFFFF
=> {:black=>"#000000", :white=>"#FFFFFF"}
>> exit

ri Hash.each gives you the signature if you need a reminder.

Methods You’ll Use Often

A handful of hash methods you’ll reach for all the time.

Reading a value safely with fetch:

hash[key] returns nil if the key is missing. fetch raises an error instead, which is often what you actually want. You can also pass a default:

$ irb
>> prices = { 'egg' => 0.1, 'butter' => 0.99 }
=> {"egg"=>0.1, "butter"=>0.99}
>> prices['milk']
=> nil
>> prices.fetch('milk')
KeyError (key not found: "milk")
>> prices.fetch('milk', 1.20)
=> 1.2
>> exit

Listing keys and values:

$ irb
>> prices = { 'egg' => 0.1, 'butter' => 0.99 }
=> {"egg"=>0.1, "butter"=>0.99}
>> prices.keys
=> ["egg", "butter"]
>> prices.values
=> [0.1, 0.99]
>> prices.size
=> 2
>> exit

Asking questions:

$ irb
>> prices = { 'egg' => 0.1, 'butter' => 0.99 }
=> {"egg"=>0.1, "butter"=>0.99}
>> prices.include?('egg')
=> true
>> prices.include?('milk')
=> false
>> prices.empty?
=> false
>> {}.empty?
=> true
>> exit

Adding, updating and deleting:

$ irb
>> prices = { 'egg' => 0.1 }
=> {"egg"=>0.1}
>> prices['butter'] = 0.99
=> 0.99
>> prices
=> {"egg"=>0.1, "butter"=>0.99}
>> prices['egg'] = 0.15
=> 0.15
>> prices.delete('egg')
=> 0.15
>> prices
=> {"butter"=>0.99}
>> exit

Merging two hashes:

$ irb
>> defaults = { color: 'white', size: 'M' }
=> {:color=>"white", :size=>"M"}
>> overrides = { color: 'red' }
=> {:color=>"red"}
>> defaults.merge(overrides)
=> {:color=>"red", :size=>"M"}
>> exit

merge returns a new hash. The original is untouched. Values in the second hash win on collisions.