Hashes
A Ruby hash is an unordered (well, insertion-ordered since 1.9) key/value collection. Rich API: each, map, merge, dig, transform_keys. Symbols are the canonical key type.
Create, access, transform, deep merge
EXAMPLE
# 1) Create
user = { name: 'Ada', email: 'a@x.com', age: 32 }
empty = {}
# String keys (older syntax)
old = { 'name' => 'Ada', 'email' => 'a@x.com' }
# Mixed
mixed = { name: 'Ada', 'phone' => '555-1234' }
# Shorthand (Ruby 3.1+) — variable name matches key
name = 'Ada'; email = 'a@x.com'
short = { name:, email: } # { name: 'Ada', email: 'a@x.com' }
# 2) Access
user[:name] # 'Ada'
user[:missing] # nil
user.fetch(:name) # 'Ada' (raises KeyError if missing)
user.fetch(:missing, 'default') # 'default'
user.fetch(:missing) { |k| "no #{k}" } # 'no missing'
# 3) Modify
user[:role] = 'admin' # add
user[:age] = 33 # update
user.delete(:age) # remove + return value
user.delete_if { |_, v| v.nil? } # conditional delete
# 4) Iterate
user.each { |k, v| puts "#{k}: #{v}" }
user.each_pair { |k, v| puts "#{k}: #{v}" }
user.each_key { |k| puts k }
user.each_value { |v| puts v }
# 5) Transform
user.map { |k, v| [k, v.to_s] }.to_h # all values to strings
user.transform_values(&:to_s) # cleaner — Ruby 2.4+
user.transform_keys(&:to_s) # symbol keys → string keys
user.transform_keys { |k| k.upcase } # custom
# Filter
user.select { |k, v| v.is_a?(String) } # keep where block true
user.reject { |_, v| v.nil? } # drop where block true
user.filter_map { |k, v| v if v.is_a?(String) } # filter + map
# Reduce
user.reduce(0) { |sum, (_, v)| sum + v.to_s.length }
user.sum { |k, v| v.to_s.length } # Ruby 2.4+
# 6) Keys + values
user.keys # [:name, :email, :age]
user.values # ['Ada', 'a@x.com', 32]
user.size # 3
user.length # alias for size
user.empty? # false
user.any? # true
user.any? { |_, v| v.is_a?(Integer) } # true
user.all? { |_, v| !v.nil? } # true
# 7) Membership
user.key?(:name) # true
user.has_key?(:name) # alias
user.include?(:name) # alias
user.value?(32) # true
# 8) Merge
a = { x: 1, y: 2 }
b = { y: 20, z: 3 }
a.merge(b) # { x: 1, y: 20, z: 3 } — b wins
b.merge(a) # { y: 2, z: 3, x: 1 } — a wins
# Block to resolve conflicts
a.merge(b) { |key, va, vb| va + vb } # { x: 1, y: 22, z: 3 }
# In-place
a.merge!(b) # mutates a
a.update(b) # alias
# 9) Deep merge (Rails / ActiveSupport)
require 'active_support/core_ext/hash/deep_merge'
a = { user: { name: 'Ada', age: 32 } }
b = { user: { email: 'a@x.com' } }
a.deep_merge(b)
# => { user: { name: 'Ada', age: 32, email: 'a@x.com' } }
# 10) dig — nested access without nil checks
user = { profile: { address: { city: 'Sydney' } } }
user.dig(:profile, :address, :city) # 'Sydney'
user.dig(:profile, :missing, :city) # nil — no error
# 11) slice + except
user = { name: 'Ada', age: 32, email: 'a@x.com', role: 'admin' }
user.slice(:name, :email) # { name: 'Ada', email: 'a@x.com' }
user.except(:role, :age) # { name: 'Ada', email: 'a@x.com' } (Rails / Ruby 3+)
# 12) Default values
h = Hash.new(0)
h[:missing] # 0
h[:foo] += 1 # 1
h[:foo] += 1 # 2
# Or with a block (different defaults per key)
h = Hash.new { |hash, key| hash[key] = [] }
h[:items] << 'a' # creates empty array, appends
h[:items] << 'b'
h # { items: ['a', 'b'] }
# Or default proc that sets dynamically
h = Hash.new { |hash, key| key.to_s.upcase }
h[:hello] # 'HELLO'
# 13) Group + count
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
numbers.group_by(&:even?) # { false: [1, 3, ...], true: [2, 4, ...] }
numbers.tally # { 1 => 1, 2 => 1, ... } — counts
['a', 'b', 'a', 'c', 'a'].tally # { 'a' => 3, 'b' => 1, 'c' => 1 }
# 14) Convert between arrays and hashes
h = { a: 1, b: 2 }
h.to_a # [[:a, 1], [:b, 2]]
h.flatten # [:a, 1, :b, 2]
h.flatten(2) # [:a, 1, :b, 2]
# From an array
[[:a, 1], [:b, 2]].to_h # { a: 1, b: 2 }
[:a, :b].zip([1, 2]).to_h # { a: 1, b: 2 }
# 15) Iterating with index
user.each_with_index do |(k, v), i|
puts "#{i}: #{k} = #{v}"
end
# 16) Sorting
user.sort_by { |k, v| v.to_s } # array of [k, v] pairs
user.min_by { |_, v| v.to_s.length } # smallest
user.max_by { |_, v| v.to_s.length } # largest
# 17) Nested hash patterns
body = {
user: {
id: 42,
profile: {
name: 'Ada',
roles: ['admin', 'user'],
},
},
posts: [
{ id: 1, title: 'Hi' },
{ id: 2, title: 'Hello' },
],
}
body.dig(:user, :profile, :name) # 'Ada'
body.dig(:user, :profile, :roles, 0) # 'admin'
body.dig(:posts, 0, :title) # 'Hi'
# 18) Symbol vs string keys — convention
# - Symbol keys for code-side data (configs, options)
# - String keys for external data (JSON, params, headers)
# - HashWithIndifferentAccess (Rails) when input may be either:
require 'active_support/core_ext/hash/indifferent_access'
h = { 'name' => 'Ada' }.with_indifferent_access
h[:name] # 'Ada'
h['name'] # 'Ada'
# 19) Pattern matching (Ruby 3+)
case user
in { name: String => name, email: String => email }
puts "#{name} <#{email}>"
in { name: }
puts "name only: #{name}"
in {}
puts "empty"
end
# Array of hashes
case rows
in [{ id: Integer => first_id, * }, *_]
puts "first id: #{first_id}"
end
# 20) JSON serialisation
require 'json'
user.to_json # '{"name":"Ada","email":"a@x.com","age":32}'
JSON.parse('{"name":"Ada"}') # { 'name' => 'Ada' } — STRING keys
JSON.parse('{"name":"Ada"}', symbolize_names: true) # { name: 'Ada' }
# 21) Hash#freeze — immutable
const = { code: 200, status: 'OK' }.freeze
# const[:code] = 500 → FrozenError
# 22) Common bugs
# • String keys vs symbol keys mismatch → undefined access
# • Mutating during iteration → undefined behaviour
# • Default values shared by reference: Hash.new([]) — same array! Use Hash.new { |h, k| h[k] = [] }
# • h[key] += 1 fails for missing keys → use Hash.new(0)
# • Comparing hashes with == checks each key/value (NOT reference)
# 23) Performance
# - Hash lookup O(1) average
# - Insertion order preserved (since 1.9)
# - Use symbols (or integers) for hot-path keys — strings allocate
# - For frozen string keys, freeze them via String#freeze or use 'literal'.freeze
# 24) Best practices
# ✅ Symbols for in-code data; strings for external
# ✅ Use fetch with defaults when key is required
# ✅ dig for nested access
# ✅ transform_keys / transform_values for type conversions
# ✅ Pattern matching for structured destructuring (Ruby 3+)
# ✅ Freeze constants intended as immutable
Why it matters
Symbols for code keys, strings for external input. dig, fetch, transform_keys/values, slice/except, and tally cover almost every hash transformation cleanly.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Example
user = { name: 'Ada', age: 36 }
puts user[:name]
user[:role] = 'admin'
user.each { |k, v| puts "#{k} = #{v}" }
Try it Yourself »
Discussion
Loading…