Hashes
Reviewed & published by Brayan K
Ruby is a dynamic, beginner-friendly programming language, and a Hash stores data as key-value pairs so you can look things up by a meaningful name instead of a position.
Part of the free Ruby course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
By the end of this lesson you'll create hashes, read and update values by key, iterate over pairs, and use defaults for missing keys.
What You'll Learn in This Lesson
- Creating hashes with symbol keys
- Reading and updating values by key
- keys, values, and key? queries
- Iterating pairs with each |k, v|
- Safe lookups with fetch and defaults
- Counting with Hash.new(0)
1️⃣ Creating, Reading, and Updating
Write a hash with { key: value }. Symbol keys (the name: form) are the norm. Read with hash[:key], and add or change a value by assigning to a key. An unknown key returns nil rather than erroring.
# A Hash maps keys to values. Symbols are the most common keys.
person = { name: "Ada", age: 36, role: :engineer }
# Look up a value by its key:
puts person[:name] # -> Ada
puts person[:age] # -> 36
# Add or update by assigning to a key:
person[:city] = "London"
person[:age] = 37
puts person.inspect # -> {:name=>"Ada", :age=>37, :role=>:engineer, :city=>"London"}
# Useful queries:
puts person.keys.inspect # -> [:name, :age, :role, :city]
puts person.values.inspect # -> ["Ada", 37, :engineer, "London"]
puts person.key?(:name) # -> true
puts person[:missing].inspect # -> nil (unknown key returns nil)2️⃣ Iterating and Defaults
Loop over pairs with each and two block parameters. Use fetch when you want a missing key to raise (or supply a default). And Hash.new(0) gives every missing key a starting value — the classic recipe for counting things.
prices = { apple: 0.5, banana: 0.25, cherry: 2.0 }
# Iterate over key/value pairs with each:
prices.each do |fruit, price|
puts "#{fruit} costs $#{price}"
end
# fetch raises if the key is missing — or returns a default you provide:
puts prices.fetch(:apple) # -> 0.5
puts prices.fetch(:mango, 0.0) # -> 0.0 (default, no error)
# Provide a default for the whole hash with Hash.new:
counts = Hash.new(0) # missing keys default to 0
"hello".each_char { |c| counts[c] += 1 }
puts counts.inspect # -> {"h"=>1, "e"=>1, "l"=>2, "o"=>1}Your turn. Manage a small shop inventory hash. Fill in the TODO lines, then run it.
# 🎯 YOUR TURN — replace each TODO, then run with: ruby inventory.rb
# A small shop inventory: item => quantity in stock
stock = { pens: 12, notebooks: 5, erasers: 0 }
# 1) Print the quantity of notebooks
puts "Notebooks: #{stock[:notebooks]}" # 👉 TODO: use the right key
# 2) Restock erasers to 20 (assign to the key)
stock[:erasers] = 20 # 👉 TODO: set a new amount
# 3) Add a new item: rulers => 8
stock[:rulers] = 8
# 4) Print every item and its quantity
stock.each do |item, qty|
puts "#{item}: #{qty}"
end
# ✅ Expected output:
# Notebooks: 5
# pens: 12
# notebooks: 5
# erasers: 20
# rulers: 8Mini-Challenge: A Word Counter
Count how many times each word appears in a sentence using Hash.new(0). Run with ruby wordcount.rb.
# 🎯 MINI-CHALLENGE: a word counter.
# Save as wordcount.rb and run with: ruby wordcount.rb
#
# 1. Start with a sentence String, e.g. "the cat sat on the mat".
# 2. Split it into words with .split.
# 3. Use Hash.new(0) so missing keys start at 0.
# 4. Loop the words, incrementing counts[word] += 1.
# 5. Print the resulting hash.
#
# ✅ Example output:
# {"the"=>2, "cat"=>1, "sat"=>1, "on"=>1, "mat"=>1}
# your code hereCommon Errors (and the fix)
- Symbol vs string key mismatch — hash[:name] and hash["name"] are different keys. Be consistent.
- nil from a missing key — Looking up an absent key gives nil, which can cause errors later. Use fetch to fail fast.
- "undefined method for nil" — Often you indexed a missing key, got nil, then called a method on it. Check the key exists.
- Forgetting the second block param — Iterate pairs with each { |k, v| ... }, not just |k|.
- Old hash-rocket syntax confusion — { :a => 1 } and { a: 1 } mean the same thing for symbol keys.
Pro Tips
- 💡 dig for nested hashes: data.dig(:user, :name) safely reaches inside without nil crashes.
- 💡 merge to combine: defaults.merge(overrides) builds settings with sensible fallbacks.
- 💡 transform values: h.transform_values { |v| v * 2 } maps over the values cleanly.
📋 Quick Reference — Hashes
| Concept | Ruby Syntax |
|---|---|
| Create | { name: "Ada" } |
| Read / write | h[:name] = "Bo" |
| Keys / values | h.keys / h.values |
| Has key? | h.key?(:name) |
| Safe lookup | h.fetch(:k, 0) |
| Iterate | h.each { |k, v| } |
🎉 Lesson Complete!
- ✅ Hashes map keys to values
- ✅ Symbol keys (name:) are the convention
- ✅ Read/write with h[:key]; missing keys return nil
- ✅ Iterate pairs with each { |k, v| }
- ✅ fetch and Hash.new(0) handle missing keys
Practice quiz
What does a Hash store?
- An ordered list indexed by number
- Only strings
- Key-value pairs looked up by key
- A single value
Answer: Key-value pairs looked up by key. A Hash maps keys to values, so you retrieve data by a meaningful key rather than a position.
Given `person = { name: "Ada" }`, what does `person[:missing]` return?
- nil
- An error
- false
- ""
Answer: nil. By default, looking up an unknown key returns nil rather than raising.
What does `{a: 1}.merge({a: 2, b: 3})` return?
- {:a=>1, :b=>3}
- {:a=>1}
- {:a=>2}
- {:a=>2, :b=>3}
Answer: {:a=>2, :b=>3}. merge overrides existing keys with the argument's values: a becomes 2 and b is added.
What does `Hash.new(0)` give you?
- A frozen hash
- A default value of 0 for missing keys
- An array of zeros
- A KeyError on miss
Answer: A default value of 0 for missing keys. Hash.new(0) returns 0 for any missing key — perfect for counters with `counts[x] += 1`.
How does `fetch` differ from `hash[:key]` for a missing key?
- fetch raises KeyError (or returns a default you pass)
- No difference
- fetch returns nil
- fetch is slower only
Answer: fetch raises KeyError (or returns a default you pass). hash[:missing] returns nil, but hash.fetch(:missing) raises KeyError; fetch can also take a default.
Why are symbols (:name) the conventional hash keys?
- They allow duplicates
- They sort automatically
- They're immutable and memory-efficient — every :name is the same object
- They are faster to type only
Answer: They're immutable and memory-efficient — every :name is the same object. Symbols are immutable and reused, so :name is always the same object — efficient for fixed keys.
Are `hash[:name]` and `hash["name"]` the same key?
- Yes, always
- No — a symbol and a string are different keys
- Only in Ruby 3
- Only for short strings
Answer: No — a symbol and a string are different keys. A symbol key and a string key are distinct; mixing them is a common source of nil surprises.
How do you iterate over key/value pairs of a hash?
- each { |k| }
- map { |v| }
- each_value { |k, v| }
- each { |k, v| }
Answer: each { |k, v| }. Use each with two block parameters: hash.each { |key, value| ... }.
What does `person.key?(:name)` return when :name exists?
- the value
- true
- nil
- the key
Answer: true. key? returns true or false depending on whether the key is present.
Since Ruby 1.9, what order does iterating a hash follow?
- Random order
- Sorted by key
- Insertion order
- Reverse order
Answer: Insertion order. Hashes preserve insertion order, so iteration visits pairs in the order they were added.
Continue this course
- Previous: Arrays
- Next: Ranges — Express sequences of numbers and letters concisely
- Quick reference: Ruby cheat sheet › Arrays & Hashes
Frequently asked questions
What is a Hash, and how is it different from an Array?
A Hash stores data as key-value pairs, so you look things up by a meaningful key rather than a numeric position. An Array is an ordered list indexed by 0, 1, 2…; a Hash is like a dictionary where you look up 'name' to get 'Ada'. Use an array when order and position matter (a queue, a sorted list), and a hash when you want to label and retrieve data by name (a user record, a config, counts per word).
Should I use symbols or strings as hash keys?
Symbols (:name) are the conventional choice for fixed, known keys because they're immutable and memory-efficient — every :name is the same object. Strings work too and are appropriate when keys come from external or dynamic data, like parsing user input or JSON. Just be consistent: hash[:name] and hash["name"] are different keys, so mixing them is a common source of nil surprises.
What happens when I look up a key that doesn't exist?
By default, hash[:missing] returns nil rather than raising an error. That's convenient but can hide bugs. If you'd rather fail loudly, use fetch: hash.fetch(:missing) raises a KeyError. fetch also takes a default: hash.fetch(:missing, 0) returns 0 instead of raising. And Hash.new(0) sets a default value for every missing key, which is perfect for counters where you want missing keys to start at zero.
How do I loop over a hash?
Use each with two block parameters — one for the key and one for the value: hash.each { |key, value| ... }. You can also iterate just the keys with each_key, just the values with each_value, or get arrays of them with hash.keys and hash.values. Since Ruby 1.9, hashes preserve insertion order, so iteration visits pairs in the order you added them — handy and predictable.