Skip to content

Ruby Hashes Complete Guide — Symbol Keys fetch merge and Hash Syntax

DodaTech Updated 2026-06-28 9 min read

In this tutorial, you will learn about Ruby Hashes Complete Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

Ruby hashes are key-value dictionaries that use any object as a key, with symbol keys being the most efficient and idiomatic, supporting methods like fetch, merge, transform, and select.

What You'll Learn

  • Creating and accessing hashes in Ruby
  • Symbol keys vs string keys — when to use each
  • Fetch, merge, transform_keys, and other essential methods
  • Default values and the Hash.new constructor

Why It Matters

Hashes are fundamental to Ruby programming. Rails controllers use hashes for params, configuration systems use nested hashes, and API responses are parsed into hashes. Durga Antivirus Pro uses hashes to map threat signatures to remediation actions. Doda Browser stores user preferences as hashes. Understanding hash methods efficiently is critical for daily Ruby work.

Real-World Use

An e-commerce Rails app uses hashes for product attributes, shopping cart items (product_id => quantity), and session data. APIs return JSON parsed into hashes. A data processing script uses hashes as lookup tables for millions of records.

flowchart LR
    A["Hashes"] --> B["Creation"]
    B --> C["Access & Fetch"]
    C --> D["Modification"]
    D --> E["Transformation"]
    E --> F["Methods"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style F fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Creating Hashes

# Empty hash
h = {}
h = Hash.new

# With initial values
h = { "name" => "Alice", "age" => 25 }

# Symbol keys (preferred)
h = { name: "Alice", age: 25 }

# Hash with default value
h = Hash.new(0)
h[:count]  # 0 (default)

# Hash with block default
h = Hash.new { |hash, key| hash[key] = key.to_s.upcase }
h[:hello]  # "HELLO"

Accessing Values

person = { name: "Alice", age: 25, city: "New York" }

# Bracket access
puts person[:name]   # "Alice"
puts person[:country]  # nil (key doesn't exist)

# Fetch — raises error or returns default
puts person.fetch(:name)        # "Alice"
puts person.fetch(:country, "Unknown")  # "Unknown"
# person.fetch(:country)  # KeyError!

# Values and keys
puts person.keys.inspect    # [:name, :age, :city]
puts person.values.inspect  # ["Alice", 25, "New York"]

# Check existence
puts person.key?(:name)     # true
puts person.has_key?(:country)  # false
puts person.value?("Alice") # true

Adding and Modifying

h = { a: 1, b: 2 }

# Add or update
h[:c] = 3
puts h.inspect  # {:a=>1, :b=>2, :c=>3}

# Merge
h.merge({ d: 4, e: 5 })
puts h.inspect  # {:a=>1, :b=>2, :c=>3} (original unchanged)

# Merge! (destructive)
h.merge!({ d: 4, e: 5 })
puts h.inspect  # {:a=>1, :b=>2, :c=>3, :d=>4, :e=>5}

# Conditional assignment
h[:a] = 100      # overwrites
h[:f] ||= 6      # assigns only if nil or missing
h.fetch(:g, 7)   # returns 7 without modifying

Iterating Over Hashes

person = { name: "Alice", age: 25, city: "New York" }

# Each
person.each do |key, value|
  puts "#{key}: #{value}"
end

# Each key
person.each_key { |key| puts key }

# Each value
person.each_value { |value| puts value }

# Map over hash
uppercase = person.map { |key, value| value.to_s.upcase }
puts uppercase.inspect
# ["ALICE", "25", "NEW YORK"]

Hash Transformation Methods

h = { a: 1, b: 2, c: 3 }

# Select
result = h.select { |key, value| value > 1 }
puts result.inspect  # {:b=>2, :c=>3}

# Reject
result = h.reject { |key, value| value > 1 }
puts result.inspect  # {:a=>1}

# Transform keys
result = h.transform_keys { |key| key.to_s.upcase }
puts result.inspect  # {"A"=>1, "B"=>2, "C"=>3}

# Transform values
result = h.transform_values { |value| value * 2 }
puts result.inspect  # {:a=>2, :b=>4, :c=>6}

# Slice (pick specific keys)
result = h.slice(:a, :c)
puts result.inspect  # {:a=>1, :c=>3}

Hash Default Values

# Default object (shared — be careful!)
h = Hash.new([])
h[:a] << 1
h[:b] << 2
puts h.inspect  # {} — h[:a] returned the default but didn't assign it!

# Default block (creates new object each time)
h = Hash.new { |hash, key| hash[key] = [] }
h[:a] << 1
h[:b] << 2
puts h.inspect  # {:a=>[1], :b=>[2]}

Nested Hashes

# Deep hash
config = {
  database: {
    host: "localhost",
    port: 5432,
    credentials: {
      user: "admin",
      password: "secret"
    }
  },
  cache: {
    adapter: "redis",
    ttl: 3600
  }
}

# Access nested values
puts config.dig(:database, :host)  # "localhost"
puts config.dig(:database, :credentials, :user)  # "admin"
puts config.dig(:missing, :key)    # nil (no error)

Hash Sorting

h = { c: 3, a: 1, b: 2 }

# Sort by key
puts h.sort.to_h.inspect
# {:a=>1, :b=>2, :c=>3}

# Sort by value
puts h.sort_by { |key, value| value }.to_h.inspect
# {:a=>1, :b=>2, :c=>3}

# Sort by value descending
puts h.sort_by { |key, value| -value }.to_h.inspect
# {:c=>3, :b=>2, :a=>1}

Converting Between Hashes and Arrays

# Hash to array of pairs
h = { a: 1, b: 2 }
puts h.to_a.inspect
# [[:a, 1], [:b, 2]]

# Array of pairs to hash
arr = [[:x, 10], [:y, 20]]
puts arr.to_h.inspect
# {:x=>10, :y=>20}

# Hash to JSON string (requires json library)
require 'json'
puts h.to_json
# {"a":1,"b":2}

Common Hash Idioms

# Counting with hashes
words = %w[apple banana apple cherry banana apple]
counts = Hash.new(0)
words.each { |word| counts[word] += 1 }
puts counts.inspect
# {"apple"=>3, "banana"=>2, "cherry"=>1}

# Grouping
numbers = [1, 2, 3, 4, 5, 6]
grouped = numbers.group_by { |n| n.even? ? :even : :odd }
puts grouped.inspect
# {:odd=>[1, 3, 5], :even=>[2, 4, 6]}

# Inverting
h = { a: 1, b: 2, c: 3 }
puts h.invert.inspect
# {1=>:a, 2=>:b, 3=>:c}

Symbol Keys vs String Keys

# Symbol keys (faster, immutable, idiomatic)
s = { name: "Alice", age: 25 }
puts s[:name]   # "Alice"

# String keys (used for JSON/YAML interop)
t = { "name" => "Alice", "age" => 25 }
puts t["name"]  # "Alice"

# They don't mix!
puts s["name"]  # nil — different key type!

# Convert
t.transform_keys(&:to_sym)  # string keys to symbol keys
s.transform_keys(&:to_s)    # symbol keys to string keys

# Symbolize keys (Rails/ActiveSupport)
# params.symbolize_keys if using ActiveSupport

Common Mistakes

1. Confusing Symbol and String Keys

hash = { name: "Alice" }
puts hash["name"]  # nil — keys are symbols, not strings!
puts hash[:name]   # "Alice" — correct access

2. Using Hash.new with Mutable Default

# Wrong — same array object shared
h = Hash.new([])
h[:a] << 1
h[:b] << 2
puts h[:a]  # [1, 2] — contamination!

# Right — new array per key
h = Hash.new { |hash, key| hash[key] = [] }
h[:a] << 1
h[:b] << 2
puts h[:a]  # [1]

3. Not Using Fetch for Safe Access

# Dangerous — returns nil silently
config[:missing_key]

# Safe — raises KeyError
config.fetch(:missing_key)

# Safe with default
config.fetch(:missing_key, "default")

4. Modifying Hash During Iteration

# Wrong
h = { a: 1, b: 2, c: 3 }
h.each { |k, v| h.delete(k) if v > 1 }

# Right
h.reject { |k, v| v > 1 }

5. Using Each When TransformValues Is Better

# Overly verbose
h.each { |k, v| h[k] = v * 2 }

# Clean
h.transform_values { |v| v * 2 }

Practice Questions

1. What's the difference between hash[:key] and hash.fetch(:key)?

hash[:key] returns nil if the key doesn't exist (silently). hash.fetch(:key) raises KeyError if the key doesn't exist, unless you provide a default: hash.fetch(:key, "default").

2. How do you merge two hashes?

Use hash1.merge(hash2) for a non-destructive merge, or hash1.merge!(hash2) to modify hash1 in place. Duplicate keys from hash2 overwrite hash1.

3. Why are symbol keys preferred over string keys?

Symbols are immutable, unique (same symbol has same object_id), and faster for comparisons. They're also more memory-efficient as hash keys. Strings create new objects each time.

4. How do you provide a default value for missing hash keys?

Use Hash.new(default) for a shared default, or Hash.new { |hash, key| hash[key] = value } for a computed default per key. The block form is safer for mutable defaults.

Challenge: Write a method that takes a text string and returns a hash with word frequencies (case-insensitive), sorted by frequency descending.

Solution
def word_frequency(text)
  text.downcase
      .scan(/\w+/)
      .each_with_object(Hash.new(0)) { |word, counts| counts[word] += 1 }
      .sort_by { |word, count| -count }
      .to_h
end

text = "Ruby is fun Ruby is powerful and Ruby is elegant"
result = word_frequency(text)
puts result.inspect
# {"ruby"=>3, "is"=>3, "fun"=>1, "powerful"=>1, "and"=>1, "elegant"=>1}

FAQ

{{< faq question="When should I use a hash vs a struct or OpenStruct?" >}} Use hashes for dynamic key-value data (API responses, configuration, params). Use Struct for objects with a fixed set of attributes that you want to access with dot notation. OpenStruct is slower and should be avoided in performance-sensitive code. {{< /faq >}}

{{< faq question="How do I check if a hash has a specific key?" >}} Use hash.key?(:key), hash.has_key?(:key), or hash.include?(:key). All three are equivalent. Use hash.value?(val) to check for values. {{< /faq >}}

{{< faq question="How do I merge two hashes and modify the result?" >}} Use hash1.merge!(hash2) to modify hash1 in place. Use hash1.merge(hash2) to return a new hash. You can also pass a block to resolve conflicts: hash1.merge(hash2) { |key, old, new| old } to keep original values. {{< /faq >}}

{{< faq question="What's the difference between each and map on a hash?" >}} each iterates and returns the original hash. map iterates and returns an array of transformed results. map is useful when you need to extract or transform hash data into a different structure. {{< /faq >}}

{{< faq question="How do I access deeply nested hash values safely?" >}} Use dig: hash.dig(:level1, :level2, :key). It returns nil at any level if the key doesn't exist, instead of raising NoMethodError on nil. {{< /faq >}}

Try It Yourself

# hash_demo.rb

config = {
  app: {
    name: "MyApp",
    version: "1.0.0",
    debug: true
  },
  database: {
    adapter: "postgresql",
    host: "localhost",
    pool: 5
  },
  features: %w[authentication caching reporting]
}

puts "App Name: #{config.dig(:app, :name)}"
puts "Debug: #{config.dig(:app, :debug)}"
puts "DB Host: #{config.dig(:database, :host)}"

puts "\nAll top-level keys: #{config.keys.join(', ')}"

puts "\nMerged with defaults:"
defaults = { database: { pool: 10 }, logging: true }
merged = config.merge(defaults)
puts merged.inspect

puts "\nTransform values:"
flattened = config.transform_values { |v| v.is_a?(Hash) ? v.keys : v }
puts flattened.inspect

Expected output:

App Name: MyApp
Debug: true
DB Host: localhost

All top-level keys: app, database, features

Merged with defaults:
{:app=>{:name=>"MyApp", :version=>"1.0.0", :debug=>true}, :database=>{:pool=>10}, :features=>["authentication", "caching", "reporting"], :logging=>true}

Transform values:
{:app=>[:name, :version, :debug], :database=>[:adapter, :host, :pool], :features=>["authentication", "caching", "reporting"]}

What's Next

Now that you understand hashes, learn how to define and use methods to organize your Ruby code into reusable units.

Topic Description Link
Ruby Methods def, return, splat, keyword args {{< ref "08-methods" >}}
Ruby Classes Object-oriented programming in Ruby {{< ref "09-classes" >}}
Python Dictionaries Compare Python dict operations Python

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro