← All posts

Jun 30, 2026

The Hash Methods I Actually Reach For in Ruby

Ten Hash methods that show up in my code every week, grouped by intent. The ones I wish I'd known on day one of writing Ruby.

ruby cheatsheet hash enumerable

Ruby’s Hash is the data structure I touch most often. After a decade of writing Ruby, the methods I actually reach for fall into three buckets: build, transform, and filter. Here’s the ten I use weekly, grouped that way.

Build

each_with_object({})

The shape I use whenever I’m building a hash from a collection:

# illustrative
counts = words.each_with_object(Hash.new(0)) do |word, h|
  h[word] += 1
end

The block argument order is (item, accumulator), which trips me up roughly forever. The accumulator is returned automatically, so no trailing h line. For counting specifically, see tally below — but each_with_object is the general-purpose builder.

to_h (with block)

When you have an array and want to turn each element into a key-value pair:

# illustrative
users.to_h { |u| [u.id, u.name] }
# => { 1 => "Ada", 2 => "Grace", ... }

The block returns a 2-element array. This is the one-liner replacement for each_with_object({}) { |u, h| h[u.id] = u.name }. I use it constantly for ID-to-record lookups.

merge (with block)

merge returns a new hash. The block is for collisions:

# illustrative
defaults = { timeout: 30, retries: 3 }
overrides = { timeout: 60 }
defaults.merge(overrides) { |_key, default, override| override || default }
# => { timeout: 60, retries: 3 }

The block is opt-in. Without it, the right-hand wins on conflict. With it, you decide. Useful for merging configs where you want to keep the default if the override is nil.

Transform

transform_values and transform_values!

Apply a block to every value, keep the keys:

# illustrative
prices = { apple: 1.0, banana: 0.5 }
prices.transform_values { |p| p * 1.2 }
# => { apple: 1.2, banana: 0.6 }

transform_values! mutates in place. I almost always use the non-bang version — it’s clearer, and the GC pressure from one extra hash is usually nothing.

transform_keys

Same idea, the other axis:

# illustrative
api_response = { "user_name" => "ada", "user_id" => 42 }
api_response.transform_keys(&:to_sym)
# => { user_name: "ada", user_id: 42 }

I use this most often for symbolizing keys at API boundaries. Rails has deep_symbolize_keys for nested hashes if you need it, documented at api.rubyonrails.org.

group_by (returns a hash)

group_by is on Enumerable, but the return type is a Hash, so it lives here too:

# illustrative
jobs.group_by(&:source_name)
# => { "Indeed" => [...], "WeWorkRemotely" => [...] }

Pair with transform_values { |arr| arr.size } to count, or with to_h { |k, arr| [k, arr.first] } to dedupe.

Filter

slice and except

Take or remove a subset of keys:

# illustrative
params = { name: "Ada", email: "a@b.com", admin: true }
params.slice(:name, :email)   # => { name: "Ada", email: "a@b.com" }
params.except(:admin)          # => { name: "Ada", email: "a@b.com" }

Both return new hashes. I use slice for permitted-attribute style filtering and except for stripping internal keys before serialization.

dig

Safe traversal of nested hashes:

# illustrative
response = { data: { user: { name: "Ada" } } }
response.dig(:data, :user, :name)   # => "Ada"
response.dig(:data, :company, :name) # => nil

The alternative is response[:data]&.[](:user)&.[](:name), which is unreadable. dig exists on Array and Struct too, and they chain through each other. Indispensable for parsing JSON API responses where any layer might be missing.

compact

Remove pairs whose value is nil:

# illustrative
attrs = { name: "Ada", email: nil, admin: false }
attrs.compact   # => { name: "Ada", admin: false }

false survives. Only nil is removed. I use this for building option hashes where missing values shouldn’t be sent at all.

partition

partition splits any Enumerable into two arrays based on a predicate. On a Hash, the block gets (key, value) and you get back two arrays of [key, value] pairs:

# illustrative
config = { timeout: 30, host: "localhost", port: 5432, debug: true }
numeric, other = config.partition { |_k, v| v.is_a?(Numeric) }
# numeric = [[:timeout, 30], [:port, 5432]]
# other   = [[:host, "localhost"], [:debug, true]]
numeric.to_h
# => { timeout: 30, port: 5432 }

The pair-array shape is annoying, but to_h brings you back. Useful when you need both halves and don’t want to iterate twice.

Quick reference

IntentMethodNotes
Build from collectioneach_with_object({})(item, acc) argument order
Build from pairsto_h { ... }Block returns [k, v]
Combine hashesmerge(other) { ... }Block resolves conflicts
Map valuestransform_valuesBang version mutates
Map keystransform_keys&:to_sym is the common case
Group itemsgroup_by (Enumerable)Returns a Hash
Take subsetslice(:a, :b)Returns new Hash
Remove subsetexcept(:a, :b)Returns new Hash
Safe nested accessdig(:a, :b, :c)Returns nil on miss
Drop nil valuescompactKeeps false
Split by predicatepartitionReturns array of pairs

The pattern: Hash is rich enough that you almost never need to write a for loop. If you find yourself writing result = {}; collection.each do |x| result[x.key] = ...; end, there’s a one-liner for it. Usually it’s to_h { ... } or each_with_object({}).