rubyguides

Hash

Hash(object)

Synopsis

Kernel#Hash is a module method on Kernel, mixed into every Object because Kernel is included at the top of the class chain. It accepts exactly one argument and coerces that argument into a Hash using a small, strict rule set. The rule set is narrow on purpose; Kernel#Hash is not a fallback for every hash-shaped value.

Hash(object) # => Hash

Behavior

The method walks four checks in order and stops at the first one that matches:

  1. If object is already a Hash, return the same object reference. No copy is made, so identity is preserved and later mutation of the result affects the original.
  2. If object is nil or an empty array [], return a new empty Hash {}. This branch is hard-coded in the C implementation and applies only to those two values, not to every empty or falsy input.
  3. If object.respond_to?(:to_hash) and to_hash returns a Hash, return that hash. to_hash is Ruby’s strict protocol for hash-like objects.
  4. Otherwise, raise TypeError with a message saying the argument cannot be converted into a Hash.

Most people are caught off guard by how strict the third rule is. An object whose to_hash returns a non-Hash value is treated as a failure, the same outcome as having no to_hash method at all, and that differs from the looser to_h protocol used by Hash#to_h. Returning a non-Hash from to_hash puts you in a worse spot than not implementing it at all.

The first rule is strict in a different way. There is no copy, no freeze, no defensive duplication. The result is identical to the input by equal?. If you want a fresh copy, ask for one explicitly.

All four branches in one snippet:

h = {a: 1}
Hash(h).equal?(h)  # => true            # rule 1: same object reference
Hash(nil)          # => {}              # rule 2: nil special case
Hash([])           # => {}              # rule 2: empty array special case
Hash(42)           # raises TypeError   # rule 4: no conversion path

Rule 1 is verified with equal? on a single stored reference. Rules 2 covers both the nil and empty-array special cases. Rule 4 is the catch-all. Rule 3 (a working to_hash) is rare in practice; the OpenStruct example below is the more common case where it does not exist.

Examples

The basic conversions, taken from the official docs:

Hash({foo: 0, bar: 1})  # => {:foo=>0, :bar=>1}
Hash(nil)               # => {}
Hash([])                # => {}

These cover the first two rules in the behavior list. The first example shows a hash passed through unchanged. The second and third cover the nil and empty-array special case, which both return the same empty {} value, even though they come from very different inputs.

Kernel#Hash does not copy a Hash argument. It returns the same object, which you can verify with equal?:

h = {a: 1}
Hash(h).equal?(h) # => true

That means mutating the result mutates the input, which is a frequent source of bugs when developers assume they got a fresh copy. If you need a defensive copy, reach for Hash#dup, Hash.new, or Hash#merge with an empty hash.

Symbols, integers, floats, and non-empty arrays all raise TypeError. None of these define to_hash, so rule 3 never fires and rule 4 takes over:

Hash(:foo)  # raises TypeError: can't convert Symbol to Hash
Hash(42)    # raises TypeError
Hash([:a])  # raises TypeError

Notice the asymmetry with the earlier example. Hash([]) returned {} because of the empty-array special case, but Hash([:a]) raises. Arrays do not define to_hash at all, so the only way an array can produce a hash is through that special case.

OpenStruct defines to_h, not to_hash, so it raises here even though it has a hash-like method:

require "ostruct"
Hash(OpenStruct.new(a: 1)) # raises TypeError

This is the most common gotcha for Kernel#Hash. If you want a hash from an OpenStruct, call OpenStruct.new(a: 1).to_h instead. The same applies to Struct, ENV, and any other object that implements only the loose to_h protocol.

A Hash subclass is still a Hash, so it is returned as-is, and its subclass identity is kept on the way out:

class MyHash < Hash; end
Hash(MyHash.new) # => #<MyHash:{}>

The class is preserved, so Hash(MyHash.new).class is MyHash, not Hash. This matters when downstream code checks is_a?(Hash) against a specific subclass or relies on subclass-specific methods.

Zero arguments raises ArgumentError because rule 1 through 4 each require an argument to inspect:

Hash()    # raises ArgumentError: wrong number of arguments (given 0, expected 1)

When to use

Kernel#Hash is the right tool when you want to normalize an argument to a hash and raise on anything unexpected. It sits between three other common hash-coercion paths, and the differences are worth knowing:

  • Hash(x) is strict, raises on failure, and special-cases nil and [].
  • x.to_h is loose, works on OpenStruct, Struct, ENV, and nil.
  • Hash[x] is a class method that builds a hash from an array of pairs.

A side-by-side on the same input:

require "ostruct"
o = OpenStruct.new(name: "Ada", age: 36)

o.to_h       # => {:name=>"Ada", :age=>36}   # loose protocol
Hash(o)      # raises TypeError              # strict protocol, no to_hash
Hash[o.to_h] # => {:name=>"Ada", :age=>36}   # Hash[] copies an existing hash

OpenStruct only ships to_h, so the middle call is the one that surprises people. Use to_h when you want the loose protocol, Hash[] when you have pairs, and Kernel#Hash only when you know the input either is a Hash or implements to_hash.

For converting user input or untrusted data, Kernel#Hash is often the safest choice because it surfaces bad inputs as exceptions and never silently produces {}. For library APIs that need to be permissive, to_h is usually the better fit.

A common pattern at an API boundary is to wrap the call so a clean error type comes out:

def require_hash(value)
  Hash(value)
rescue TypeError
  raise ArgumentError, "expected a Hash, got #{value.class}"
end

Common gotchas

  • Same object, not a copy. Hash(some_hash) returns the same reference. Mutations leak in both directions.
  • Empty array is magic. Hash([]) returns {}, but Hash([:a]) raises. The empty-array special case does not generalize to other empty values such as "" or 0.
  • to_hash versus to_h. Kernel#Hash uses the strict to_hash conversion. Hash#to_h uses the looser to_h protocol. Objects that implement only to_h (such as OpenStruct, Struct, and ENV) raise TypeError here.
  • No exception: keyword. Unlike Kernel#Integer and Kernel#Float, Kernel#Hash has no exception: false fallback. Conversion failure always raises.
  • No block form. You cannot pass a block to compute a default. For that, reach for Hash.new(0) or Hash.new { |h, k| h[k] = [] }.
  • No zero-argument form. Hash() raises ArgumentError. You must pass exactly one argument.
  • Not a constructor. Kernel#Hash is unrelated to Hash.new, to the Hash[...] class method, and to the {} literal. The only thing it does is coerce a single argument.
  • Hash subclasses are preserved. A Hash subclass instance is returned with its class intact. It is not converted to a base Hash.

A quick walkthrough of several of these in one block:

h = {a: 1}
Hash(h).equal?(h)            # => true            # same-object, not a copy
Hash(h)[:b] = 2              # also mutates h     # leak in both directions
Hash([])                     # => {}              # empty-array special case
Hash([:a])                   # raises TypeError   # non-empty array has no to_hash
Hash()                       # raises ArgumentError # zero-arg form is not allowed

Line 1 is the identity check. Line 2 shows the practical consequence: writing to the result writes to the original h too. Lines 3 and 4 highlight the asymmetry between empty and non-empty arrays. Line 5 confirms that the zero-argument form is a hard error, separate from the TypeError raised by bad input.

See also

  • Kernel#Array — sibling Kernel conversion that walks the to_ary then to_a fallback chain.
  • Kernel#Integer — sibling Kernel conversion that supports the exception: keyword, which Kernel#Hash deliberately does not.
  • Hash#to_h — the looser to_h protocol that Kernel#Hash does not delegate to.