← Reference · Nestor G Pestelos Jr · Print this page
Programming Languages · Ruby
Ruby User-Defined Hash Keys
Reference entry · last updated September 21, 2026
A user-defined hash key in Ruby is an object whose equality the program defines. A Hash calls the key's hash method to choose a bucket, then calls eql? to decide whether a key already in that bucket is the same key. The inherited Object versions make every object a distinct key, so a class whose instances should match by value overrides both methods. Overriding eql? alone is not enough: two equal keys that hash differently land in different buckets and both survive as separate keys.[1][2]
First principles and definitions
Identity and equality
Ruby separates three questions. equal? asks whether two references point at the same object, and it is never overridden. == asks whether two objects are equal, and classes define what that means. eql? is the stricter form that a Hash uses, and for Object it is synonymous with ==.[2]
The split between == and eql? is easiest to see with numbers. == converts across numeric types, but eql? does not, which keeps 1 and 1.0 as different hash keys.[2]
1 == 1.0 # => true 1.eql?(1.0) # => false
How a Hash looks a key up
To be usable as a hash key, an object must implement hash and eql?. The hash value selects a bucket, and eql? is then used to test members inside that bucket for equality. Object#hash returns an Integer for this purpose.[1][2]
This two-step lookup is why both methods must agree. The bucket choice is a fast filter, and eql? is the exact test. A key that skips the first step is never compared.
The hash and eql? invariant
The required property is: if a.eql?(b) is true, then a.hash must equal b.hash.[2] Two objects that are equal by eql? but hash differently break the lookup, because they never reach the same bucket.
eql? should also be symmetric and transitive, and the pair must stay consistent: two keys that are eql? must return the same hash, and the hash value must not change while the key is stored.
Overriding eql? and hash
Override both when two distinct objects should be the same key because their data is equal, and identity is not the point. Typical cases are a domain value object, a composite identifier used for deduplication, or a cache key.
Derive hash from the fields used for equality. Aliasing eql? to == is safe only when the field comparisons agree with their hashes. Checking the outer class does not enforce this: 1 == 1.0 is true, while 1.eql?(1.0) is false. This example compares each field with eql?.[2]
class Book
attr_reader :author, :title
def initialize(author, title)
@author = author
@title = title
end
def ==(other)
self.class == other.class &&
other.author.eql?(@author) &&
other.title.eql?(@title)
end
alias eql? ==
def hash
[self.class, @author, @title].hash
end
end
book1 = Book.new('matz', 'Ruby in a Nutshell')
book2 = Book.new('matz', 'Ruby in a Nutshell')
reviews = {}
reviews[book1] = 'Great reference!'
reviews[book2] = 'Nice and compact!'
reviews.length # => 1
Without the overrides, book1 and book2 are two distinct keys and the hash holds two entries.
When it does not make sense
- Identity is the intended key. The inherited
Object#eql?andObject#hashalready make each object a distinct key. Overriding collapses keys that should stay separate. - The key is already a value type. String, Symbol, Integer, and Array define value equality and a matching
hash, so a tuple key such as[id, type]already deduplicates by value. - The equality fields are mutable. Changing a field that feeds
hashafter the key is stored corrupts its bucket. This is the practical reason to keep keys immutable. - The hash opted into identity.
compare_by_identitymakes a hash compare keys by object identity and ignorehashandeql?.[1]
Alternatives
Struct bundles members and defines member-based eql? and hash, so its instances work as value keys without hand-written overrides.[3] Data, added in Ruby 3.2, is the immutable counterpart for the same purpose.[4]
When the key is a composite, a plain Array or a joined String is often simpler than a custom class, because the built-in classes already satisfy the contract. Reach for a custom key when the object carries behavior or identity that the key must represent.
Choosing
- Each object is a distinct key: do nothing, keep the inherited methods.
- Value equality is wanted and the key is simple: use a built-in type such as Array or String.
- A value object is the key: use
Struct, or defineeql?andhashfrom the same fields. Aliaseql?to==only if its field comparisons satisfy the hash contract. - Identity is wanted on purpose: call
compare_by_identityinstead of overriding equality. - Keys stay immutable: never mutate a field that participates in
hashafter insertion.
References
- ^ Ruby 3.3 core documentation, "Hash," User-Defined Hash Keys and
compare_by_identity. Free full text: docs.ruby-lang.org/en/3.3/Hash.html - ^ Ruby 3.3 core documentation, "Object,"
eql?,hash, andequal?. Free full text: docs.ruby-lang.org/en/3.3/Object.html - ^ Ruby 3.3 core documentation, "Struct." Free full text: docs.ruby-lang.org/en/3.3/Struct.html
- ^ Ruby 3.3 core documentation, "Data." Free full text: docs.ruby-lang.org/en/3.3/Data.html