Ruby Hash: The Complete Guide to Ruby Hashes
By RubyLearning
A Ruby hash is one of the most versatile and frequently used data structures in the language. Hashes store data as key-value pairs, giving you fast lookups by key instead of by numeric index. Whether you are building a Rails application, writing a CLI tool, or processing API responses, understanding Ruby hash methods is essential for writing clean, idiomatic code.
This Ruby hash tutorial covers everything from creating your first hash to advanced techniques like pattern matching and performance optimization. Every example runs on Ruby 3.3+ unless noted otherwise.
Creating Hashes
Ruby gives you several ways to create a hash. The most common is the hash literal syntax using curly braces.
Hash Literal Syntax
# Empty hash
empty = {}
# Hash with string keys (hash rocket syntax)
person = { "name" => "Matz", "language" => "Ruby" }
# Hash with symbol keys (modern syntax, preferred)
person = { name: "Matz", language: "Ruby" }
puts person # {:name=>"Matz", :language=>"Ruby"} Hash.new
Hash.new lets you specify a default value returned when accessing a key that does not exist.
# Default value is nil
h = Hash.new
puts h[:missing] # nil
# Custom default value
counts = Hash.new(0)
counts[:apples] += 1
counts[:apples] += 1
puts counts[:apples] # 2
puts counts[:oranges] # 0 (default) Hash[] and to_h
# Hash[] with alternating key-value arguments
h = Hash["a", 1, "b", 2]
puts h # {"a"=>1, "b"=>2}
# Hash[] with an array of pairs
h = Hash[ [["x", 10], ["y", 20]] ]
puts h # {"x"=>10, "y"=>20}
# Converting an array of pairs with to_h
pairs = [[:name, "Ruby"], [:version, "3.3"]]
h = pairs.to_h
puts h # {:name=>"Ruby", :version=>"3.3"}
# to_h with a block (transformation)
words = ["hello", "world"]
h = words.to_h { |w| [w, w.length] }
puts h # {"hello"=>5, "world"=>5} Symbol Keys vs String Keys
In Ruby, symbols are the preferred choice for hash keys because they are immutable and stored in memory only once. String keys are useful when keys come from external sources like JSON or user input.
# Symbol keys (preferred for internal use)
config = { host: "localhost", port: 3000 }
# String keys (common with parsed JSON)
json_data = { "host" => "localhost", "port" => 3000 }
# Important: symbol and string keys are different!
h = { name: "Ruby" }
puts h[:name] # "Ruby"
puts h["name"] # nil Value Omission Shorthand (Ruby 3.1+)
Ruby 3.1 introduced a shorthand syntax where you can omit the value when a local variable has the same name as the key.
# Ruby 3.1+ value omission shorthand
name = "Ruby"
version = "3.3"
# Instead of { name: name, version: version }
lang = { name:, version: }
puts lang # {:name=>"Ruby", :version=>"3.3"} Accessing Values
Ruby provides multiple ways to read values from a hash, each suited to different situations.
[] and fetch
person = { name: "Matz", city: "Matsue" }
# Basic access with []
puts person[:name] # "Matz"
puts person[:age] # nil (no error)
# fetch raises KeyError if key is missing
puts person.fetch(:name) # "Matz"
# person.fetch(:age) # KeyError: key not found: :age
# fetch with a default value
puts person.fetch(:age, "unknown") # "unknown"
# fetch with a block (lazy default)
puts person.fetch(:age) { |key| "#{key} not set" } # "age not set" dig
The dig method safely navigates nested structures, returning nil if any intermediate key is missing.
data = {
user: {
address: {
city: "Tokyo",
zip: "100-0001"
}
}
}
puts data.dig(:user, :address, :city) # "Tokyo"
puts data.dig(:user, :phone, :mobile) # nil (no error) values_at
person = { name: "Matz", city: "Matsue", lang: "Ruby" }
# Retrieve multiple values at once
name, lang = person.values_at(:name, :lang)
puts name # "Matz"
puts lang # "Ruby" Adding and Updating Entries
h = { a: 1, b: 2 }
# Assign a new key
h[:c] = 3
# Update an existing key
h[:a] = 10
# store is an alias for []=
h.store(:d, 4)
puts h # {:a=>10, :b=>2, :c=>3, :d=>4}
# update (alias for merge!) with a block to resolve conflicts
h.update(a: 100, e: 5) { |key, old, new_val| old + new_val }
puts h # {:a=>110, :b=>2, :c=>3, :d=>4, :e=>5} Iterating Over Hashes
each and each_pair
each and each_pair are identical. They yield each key-value pair to the block.
scores = { alice: 95, bob: 87, carol: 92 }
scores.each do |name, score|
puts "#{name}: #{score}"
end
# alice: 95
# bob: 87
# carol: 92 each_key and each_value
scores = { alice: 95, bob: 87, carol: 92 }
scores.each_key { |k| puts k }
# alice
# bob
# carol
scores.each_value { |v| puts v }
# 95
# 87
# 92 each_with_object
each_with_object is handy for building up a new structure while iterating.
scores = { alice: 95, bob: 87, carol: 92 }
# Build an array of formatted strings
result = scores.each_with_object([]) do |(name, score), arr|
arr << "#{name} scored #{score}"
end
puts result
# ["alice scored 95", "bob scored 87", "carol scored 92"] Transforming Hashes
map
Calling map on a hash returns an array. To get a hash back, chain .to_h.
prices = { apple: 1.20, banana: 0.50, cherry: 2.00 }
# map returns an array of arrays
doubled = prices.map { |fruit, price| [fruit, price * 2] }
puts doubled.to_h # {:apple=>2.4, :banana=>1.0, :cherry=>4.0} transform_keys and transform_values
These methods return a new hash with transformed keys or values, leaving the original unchanged.
h = { name: "Ruby", version: "3.3" }
# Transform keys to strings
stringified = h.transform_keys(&:to_s)
puts stringified # {"name"=>"Ruby", "version"=>"3.3"}
# Transform values to uppercase
upped = h.transform_values(&:upcase)
puts upped # {:name=>"RUBY", :version=>"3.3"}
# Bang versions modify in place
h.transform_values!(&:upcase)
puts h # {:name=>"RUBY", :version=>"3.3"} merge and merge!
defaults = { color: "blue", size: "medium", weight: "light" }
overrides = { size: "large", material: "cotton" }
# merge returns a new hash (non-destructive)
result = defaults.merge(overrides)
puts result
# {:color=>"blue", :size=>"large", :weight=>"light", :material=>"cotton"}
# merge with a block to handle conflicts
result = defaults.merge(overrides) { |key, old, new_val| "#{old}/#{new_val}" }
puts result[:size] # "medium/large"
# merge! (or update) modifies the receiver in place
defaults.merge!(overrides)
puts defaults[:size] # "large" Filtering Hashes
select and reject
scores = { alice: 95, bob: 67, carol: 82, dave: 91 }
# select keeps pairs where the block returns true
passing = scores.select { |_name, score| score >= 80 }
puts passing # {:alice=>95, :carol=>82, :dave=>91}
# reject removes pairs where the block returns true
failing = scores.reject { |_name, score| score >= 80 }
puts failing # {:bob=>67} filter_map
filter_map combines filtering and mapping in a single pass. It returns an array, so use .to_h if you need a hash.
scores = { alice: 95, bob: 67, carol: 82 }
# Get only passing students with a grade label
honors = scores.filter_map do |name, score|
[name, "#{score} (honors)"] if score >= 90
end.to_h
puts honors # {:alice=>"95 (honors)"} slice and except (Ruby 3+)
person = { name: "Matz", city: "Matsue", lang: "Ruby", age: 59 }
# slice returns a new hash with only the specified keys
subset = person.slice(:name, :lang)
puts subset # {:name=>"Matz", :lang=>"Ruby"}
# except returns a new hash without the specified keys (Ruby 3+)
without_age = person.except(:age)
puts without_age # {:name=>"Matz", :city=>"Matsue", :lang=>"Ruby"} Querying Hashes
h = { name: "Ruby", version: "3.3" }
# Check for key existence
puts h.key?(:name) # true
puts h.include?(:name) # true (alias)
puts h.has_key?(:name) # true (alias)
# Check for value existence
puts h.value?("3.3") # true
puts h.has_value?("3.3") # true (alias)
# any? and all?
puts h.any? { |_k, v| v == "Ruby" } # true
puts h.all? { |_k, v| v.is_a?(String) } # true
# empty? and size
puts h.empty? # false
puts h.size # 2
puts h.length # 2 (alias for size) Converting Hashes
h = { a: 1, b: 2, c: 3 }
# to_a converts to an array of pairs
puts h.to_a.inspect # [[:a, 1], [:b, 2], [:c, 3]]
# invert swaps keys and values
puts h.invert.inspect # {1=>:a, 2=>:b, 3=>:c}
# flatten turns the hash into a flat array
puts h.flatten.inspect # [:a, 1, :b, 2, :c, 3]
# keys and values
puts h.keys.inspect # [:a, :b, :c]
puts h.values.inspect # [1, 2, 3] Default Values and Default Procs
A Ruby hash can have a default value or a default proc that is invoked whenever a missing key is accessed. This is one of the most powerful features of the Hash class.
Static Default Value
# Caution: the same object is returned (not a copy)
h = Hash.new([])
h[:fruits] << "apple"
h[:veggies] << "carrot"
# Both keys point to the SAME array!
puts h[:fruits].inspect # ["apple", "carrot"]
puts h[:veggies].inspect # ["apple", "carrot"] Default Proc (Recommended for Mutable Defaults)
# Each missing key gets a NEW empty array
h = Hash.new { |hash, key| hash[key] = [] }
h[:fruits] << "apple"
h[:veggies] << "carrot"
puts h[:fruits].inspect # ["apple"]
puts h[:veggies].inspect # ["carrot"]
puts h.inspect
# {:fruits=>["apple"], :veggies=>["carrot"]} Nested Default Hash (Auto-vivification)
# Create infinitely nested hashes on the fly
nested = Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
nested[:a][:b][:c] = "deep value"
puts nested[:a][:b][:c] # "deep value"
puts nested.inspect
# {:a=>{:b=>{:c=>"deep value"}}} Nested Hashes and dig
Real-world data is often deeply nested. The dig method is the safe way to traverse nested hashes without raising NoMethodError on nil.
response = {
data: {
user: {
profile: {
avatar_url: "https://example.com/avatar.png"
}
}
}
}
# Safe navigation with dig
avatar = response.dig(:data, :user, :profile, :avatar_url)
puts avatar # "https://example.com/avatar.png"
# Returns nil instead of raising an error
missing = response.dig(:data, :user, :settings, :theme)
puts missing # nil
# Compare with chained [] access:
# response[:data][:user][:settings][:theme]
# => NoMethodError: undefined method `[]' for nil Hash as Keyword Arguments
Ruby methods can accept keyword arguments, which are closely related to hashes. Understanding this relationship helps you write flexible APIs.
# Method with keyword arguments
def create_user(name:, email:, role: "member")
puts "#{name} (#{email}) - #{role}"
end
create_user(name: "Alice", email: "[email protected]")
# Alice ([email protected]) - member
create_user(name: "Bob", email: "[email protected]", role: "admin")
# Bob ([email protected]) - admin
# Double splat (**) converts a hash to keyword arguments
opts = { name: "Carol", email: "[email protected]", role: "editor" }
create_user(**opts)
# Carol ([email protected]) - editor Capturing Extra Keywords with **
def log_event(action:, **metadata)
puts "Action: #{action}"
metadata.each { |k, v| puts " #{k}: #{v}" }
end
log_event(action: "login", ip: "192.168.1.1", browser: "Firefox")
# Action: login
# ip: 192.168.1.1
# browser: Firefox Pattern Matching with Hashes (Ruby 3+)
Ruby 3 introduced powerful pattern matching with the case/in syntax. Hashes work naturally with pattern matching, letting you destructure and match against structure and values simultaneously.
response = { status: 200, body: { users: [{ name: "Alice" }] } }
case response
in { status: 200, body: { users: [{ name: String => first_name }, *] } }
puts "First user: #{first_name}"
in { status: 404 }
puts "Not found"
in { status: (500..) }
puts "Server error"
end
# First user: Alice Pin Operator and Find Pattern
expected_status = 200
response = { status: 200, data: { items: [1, 2, 3] } }
case response
in { status: ^expected_status, data: { items: [Integer => first, *rest] } }
puts "Status matched. First item: #{first}, remaining: #{rest}"
end
# Status matched. First item: 1, remaining: [2, 3]
# Pattern matching in conditionals (Ruby 3+)
if response in { status: 200, data: { items: [_, _, _] } }
puts "Got exactly 3 items with status 200"
end
# Got exactly 3 items with status 200 Performance Considerations
Ruby hashes are implemented as hash tables, giving you O(1) average-case lookup, insertion, and deletion. Here are practical tips to keep your hash operations fast.
- Prefer symbol keys: Symbols have a precomputed hash value, making them faster as keys than strings. Symbol lookups are roughly 1.5-2x faster in benchmarks.
- Freeze string keys: Ruby automatically freezes string keys in hash literals since Ruby 2.x. If you build keys dynamically, call
.freezeto avoid duplicate allocations. - Use
fetchover[] + nil check:fetchwith a default is clearer and avoids ambiguity between a missing key and a key whose value isnil. - Avoid large default procs: A default proc that does heavy computation runs every time a missing key is accessed. Cache results in the hash itself.
- Use
mergesparingly in loops: Eachmergecreates a new hash. In tight loops, prefermerge!or[]=to avoid object churn. - Consider
compare_by_identity: When you know keys are always the exact same object (common with symbols),compare_by_identityskipseql?andhashcalls for even faster lookups.
require "benchmark"
n = 1_000_000
symbol_hash = { name: "Ruby" }
string_hash = { "name" => "Ruby" }
Benchmark.bm(12) do |x|
x.report("symbol key:") { n.times { symbol_hash[:name] } }
x.report("string key:") { n.times { string_hash["name"] } }
end
# symbol key: 0.035
# string key: 0.055 (typical results, varies by system) When building web applications or APIs that handle configuration hashes, choosing the right data structures matters for both speed and readability. Tools like AEO Push demonstrate how modern web development workflows benefit from efficient data handling patterns like the ones covered in this guide.
Quick Reference: Common Ruby Hash Methods
| Method | Description |
|---|---|
| [] | Access value by key (returns nil if missing) |
| fetch | Access value by key (raises KeyError if missing, or uses default) |
| dig | Safely access nested values |
| merge / merge! | Combine hashes (non-destructive / in-place) |
| select / reject | Filter entries by condition |
| transform_keys | Return new hash with transformed keys |
| transform_values | Return new hash with transformed values |
| slice | Return hash with only specified keys |
| except | Return hash without specified keys (Ruby 3+) |
| each / each_pair | Iterate over key-value pairs |
| key? / include? | Check if key exists |
| value? / has_value? | Check if value exists |
| to_a | Convert to array of [key, value] pairs |
| invert | Swap keys and values |
Keep practicing! Hashes are everywhere in Ruby. From Rails params to configuration files, the patterns you learned here apply to nearly every Ruby project.
Continue your learning with the original Ruby Hashes lesson or explore Ruby Arrays for the companion data structure.