Hashes in Ruby
A Hash is Ruby's associative array — an insertion-ordered collection of key-value pairs, where any object can serve as a key as long as it implements hash and eql? consistently. In practice, symbols are the overwhelmingly common key type because they're immutable and memory-efficient, and modern Ruby offers a shorthand syntax specifically for symbol keys. Since Ruby 1.9, hashes preserve insertion order when iterated, which makes them reliable for building ordered reports or preserving the order fields were added.
Cricket analogy: A scorecard mapping each player's name to their runs scored is an insertion-ordered Hash, and just as a scorer relies on a consistent way to identify each player (like a jersey number) as a key, Ruby hashes need consistent hash and eql? on keys.
Literal syntax and safe access
The classic hash-rocket syntax { :key => value } works with any key type, but when keys are symbols, Ruby 1.9+ allows the more concise { key: value } form — the two are equivalent, but the shorthand cannot be used for non-symbol keys such as strings. Symbol keys and string keys are never interchangeable: { name: "Ada" }["name"] returns nil, because :name and "name" are different objects. By default hash[missing_key] returns nil, but fetch raises a KeyError on a missing key unless you supply an explicit default, making missing-key bugs loud rather than silent.
Cricket analogy: Writing { :captain => "Root" } works for any key, but the shorthand { captain: "Root" } only applies to symbol keys; a scorecard keyed by symbol :captain and one keyed by string "captain" are treated as entirely different entries, and fetch raises loudly on a missing key instead of silently returning nil.
user = { name: "Ada", role: :admin }
user[:name] # => "Ada"
user["name"] # => nil, string and symbol keys are distinct
user.fetch(:email) # raises KeyError
user.fetch(:email, "unknown") # => "unknown", with a defaultDefault values, dig, and transforming hashes
Hash.new(default) lets you set a fallback value returned for missing keys instead of nil — useful for counters — and Hash.new { |hash, key| ... } goes further, computing a default on demand. For deeply nested hashes, dig safely traverses multiple levels and returns nil instead of raising if any intermediate key is missing. Because Hash includes Enumerable, transform_values reshapes every value while keeping keys unchanged, and merge combines two hashes, with a block available to resolve key conflicts explicitly.
Cricket analogy: Hash.new(0) gives a wicket-count tallier a default of zero for any bowler not yet seen, dig safely traverses a nested { team: { squad: { player: stats } } } structure returning nil if a level is missing, and transform_values could convert every player's raw score into a strike rate.
word_counts = Hash.new(0)
"the fox the lazy fox".split.each { |w| word_counts[w] += 1 }
word_counts # => {"the"=>2, "fox"=>2, "lazy"=>1}
config = { database: { host: "localhost", port: 5432 } }
config.dig(:database, :port) # => 5432
config.dig(:database, :missing, :deep) # => nil, no NoMethodError
prices = { apple: 1.5, banana: 0.5 }
prices.transform_values { |p| (p * 0.9).round(2) } # => {apple: 1.35, banana: 0.45}Python's dict and Ruby's Hash both preserve insertion order since around the same era (Python 3.7 made it official; Ruby has done so since 1.9), but Ruby's Hash gains the entire Enumerable toolkit for free — the same map/select/reduce vocabulary used on arrays works identically on hashes.
Symbol keys and string keys are never interchangeable, which trips up developers parsing JSON: JSON.parse returns string keys by default, so a hash built from parsed JSON must be accessed with ["name"], not [:name], unless you call JSON.parse(json, symbolize_names: true).
- Hashes store key-value pairs and preserve insertion order since Ruby 1.9+.
- The
key: valueshorthand only works for symbol keys; hash-rocket=>syntax is required for other key types. fetchraises KeyError on a missing key (or accepts a default), unlike[]which silently returns nil.Hash.new(default)orHash.new { |h, k| ... }supplies a fallback for missing keys, useful for counters.digsafely traverses nested hashes/arrays, returning nil instead of raising if an intermediate key is absent.- Symbol keys and string keys are distinct — JSON-parsed hashes use string keys by default unless symbolized explicitly.
Practice what you learned
1. What does `{ name: "Ada" }["name"]` return?
2. What is the main behavioral difference between `hash[:missing_key]` and `hash.fetch(:missing_key)`?
3. What does `Hash.new(0)` provide?
4. What does `config.dig(:a, :b, :c)` do if `:b` doesn't exist inside `config[:a]`?
5. Which method reshapes every value in a hash while keeping the keys unchanged?
Was this page helpful?
You May Also Like
Arrays in Ruby
An in-depth look at Ruby's Array class — ordered, index-based collections that can hold mixed types and expose a huge Enumerable-powered method set.
Symbols Explained
What Ruby symbols are, how they differ from strings under the hood, and why they're the idiomatic choice for hash keys, method names, and identifiers.
each, map, and select
Master the three most commonly used Enumerable methods — each for side effects, map for transformation, and select for filtering — and when to reach for which.
Enumerable Deep Dive
Go beyond map and select to explore Enumerable's full toolkit — sort_by, group_by, partition, each_with_object, lazy enumerators, and more — for expressive collection processing.
Related Reading
Related Study Notes in Programming
Browse all study notesApache Spark Study Notes
Programming · 30 topics
ProgrammingApache Flink Study Notes
Programming · 30 topics
ProgrammingHadoop Study Notes
Programming · 30 topics
ProgrammingSnowflake Study Notes
Programming · 30 topics
ProgrammingApache Airflow Study Notes
Programming · 30 topics
Programmingdbt (Data Build Tool) Study Notes
Programming · 30 topics