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’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
| Intent | Method | Notes |
|---|---|---|
| Build from collection | each_with_object({}) | (item, acc) argument order |
| Build from pairs | to_h { ... } | Block returns [k, v] |
| Combine hashes | merge(other) { ... } | Block resolves conflicts |
| Map values | transform_values | Bang version mutates |
| Map keys | transform_keys | &:to_sym is the common case |
| Group items | group_by (Enumerable) | Returns a Hash |
| Take subset | slice(:a, :b) | Returns new Hash |
| Remove subset | except(:a, :b) | Returns new Hash |
| Safe nested access | dig(:a, :b, :c) | Returns nil on miss |
| Drop nil values | compact | Keeps false |
| Split by predicate | partition | Returns 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({}).