The hash toolbox

fetch instead of brackets

h[:missing] answers nil, which then fails somewhere else. fetch raises at the line that asked, or returns a fallback when one is given. A block computes the fallback only when it is needed:

ruby -e 'h = {a: 1}; p h.fetch(:b) { :missing }'

:missing

Use fetch for data that must be there — a configuration key, a field from an API — and brackets for a lookup where nil is a valid answer.

dig into a nested hash

dig walks a chain of keys and returns nil as soon as one is missing, without raising on a nil in the middle:

ruby -e 'h = {a: {b: 2}}; p h.dig(:a, :b); p h.dig(:x, :y)'

2
nil

The second call would raise NoMethodError as h[:x][:y], because h[:x] is nil. That is the whole reason dig exists, and it is the method to use on a JSON response whose shape is not guaranteed.

merge and transform

merge returns a new hash and lets the right side win; transform_keys and transform_values rewrite one side without a loop:

ruby -e 'p({a: 1, b: 2}.merge({b: 3}))'

{a: 1, b: 3}

ruby -e 'p({a: 1}.transform_keys { |k| k.to_s })'

{"a" => 1}

merge is shallow: a nested hash on the right replaces the one on the left, it does not merge inside it. Ruby 3.4 has deep_merge in ActiveSupport, not in core; for a plain project, merge the nested level by hand.

Default values

A default makes a missing key answer something useful. Hash.new(0) is the counting idiom; a block default is the one for a hash of lists:

ruby -e 'h = Hash.new { |h, k| h[k] = [] }; h[:a] << 1; p h'

{a: [1]}

The block receives the hash and the missing key, and the assignment inside it is what stores the new list. Without that assignment the block returns a fresh array each time and nothing is kept:

ruby -e 'h = Hash.new([]); h[:a] << 1; p h[:b]'

[1]

The second line is the classic trap: every missing key answers the same array, so h[:a] << 1 also changed h[:b]. The block form with the assignment is the only safe default for a mutable value.

What to watch for

keys, values and each keep insertion order, so a hash printed with p is stable and easy to compare between runs.

A hash with a default that is not in the hash is not equal to a hash that has the key: Hash.new(0)[:a] answers zero but keys stays empty. Store the value when the caller needs to see it.

Next: Nested arrays and hashes

Article author: Arthur Isaev