By default, getting the value of a missing key in a hash will return nil. Let’s take a look at how it works.

h = {}
h[:missing] # => nil

If a key is missing, #[] will run #default_proc if defined and return its result, otherwise #default will be called.

By default these methods return nil:

h = {} # => {}
h.default_proc # => nil
h.default # => nil

You can set them explicitly:

h = {}
h.default_proc = proc { |h, k| h[k] = :foo }
h[:missing] # => :foo

h = {}
h.default = :foo
h[:missing] # => :foo

Or by using Hash.new’s parameters:

h = Hash.new { |h, k| h[k] = :foo }
h[:missing] # => :foo

h = Hash.new(:foo)
h[:missing] # => :foo

Unless your default value is falsy (false and nil), || would ignore all missing keys:

h = Hash.new(:foo)
h[:missing] || :default # => :foo

h = Hash.new(false)
h[:missing] || :default # => :default

That’s why it’s preferable to use #fetch:

h = Hash.new(:foo)
h.fetch(:missing, :default) # => :default

h = Hash.new(false)
h.fetch(:missing, :default) # => :default

Hash#new(obj) → new_hash: If obj is specified, this single object will be used for all default values.

It can lead to unexpected behaviors if you want to use more complex objects as the default values.

What if we want an empty array as the default values ?

h = Hash.new([])
h[:foo] << :foo # => [:foo]
h[:bar] << :bar # => [:foo, :bar]
h[:foo] # => [:foo, :bar]
h[:foo].equal?(h[:bar]) # => true

#default_proc should be used in this case:

h = Hash.new { |h, k| h[k] = [] }
h[:foo] << :foo # => [:foo]
h[:bar] << :bar # => [:bar]
h[:foo] # => [:foo]
h[:foo].equal?(h[:bar]) # => false

We can create a hash with a dynamic depth by calling #default_proc recursively. This is also known as Autovivification.

h = {} # => {}
h[:a][:b][:c][:d] = :foo # => NoMethodError: undefined method `[]' for nil:NilClass
h = Hash.new { |hash, key| hash[key] = Hash.new(&hash.default_proc) } # => {}
h[:a][:b][:c][:d] = :foo # => :foo
h # => {:a=>{:b=>{:c=>{:d=>:foo}}}}