Hash#max_by
Hash#max_by iterates over a hash and returns the key-value pair whose block value is the largest. It’s defined in the Enumerable module, which Hash includes.
scores = { alice: 95, bob: 82, carol: 98 }
scores.max_by { |_name, score| score }
# => [:carol, 98]
Without a block, max_by compares the keys directly using their natural ordering. With a block, you provide a transformation that maps each key-value pair to a comparable value — for example, extracting a numeric field from a nested hash. This makes max_by more flexible than max because you decide what “largest” means for your particular data structure and use case. The block receives two arguments on each iteration: the key and the value, in that order, matching the convention used by each and other Hash iterators.
Basic usage
products = { "Widget" => 12.99, "Gadget" => 24.99, "Doodad" => 7.99 }
cheapest = products.min_by { |_name, price| price }
# => ["Doodad", 7.99]
priciest = products.max_by { |_name, price| price }
# => ["Gadget", 24.99]
The products example above uses min_by and max_by on the same hash, each answering a different question. max_by returns ["Gadget", 24.99] as a two-element array rather than just the value 24.99, so you typically destructure the result: name, price = products.max_by { |_n, p| p }. This destructuring pattern is consistent across all Enumerable methods that operate on hashes.
max_by returns the key-value pair as a two-element array, not the value alone.
Controlling what gets compared
The block decides the comparison. This means you can maximize on any attribute, not just the hash values:
employees = {
"101" => { name: "Alice", tenure: 6 },
"102" => { name: "Bob", tenure: 2 },
"103" => { name: "Carol", tenure: 8 },
}
employees.max_by { |_id, data| data[:tenure] }
# => ["103", {:Name=>"Carol", :tenure=>8}]
The employees example shows how max_by can look past the hash values and compare nested attributes instead. By extracting data[:tenure] from each employee record, the block instructs max_by to find the person with the longest tenure regardless of the employee ID key. This same technique works for any nested structure — just return the field you want to maximize from the block.
When values are equal, max_by returns the first pair it encountered at that maximum:
{ a: 1, b: 1, c: 2 }.max_by { |_k, v| v }
# => [:c, 2] — unique max
When multiple pairs produce the same block value, tie-breaking behavior matters. In the example above, both :a and :b would map to 1, but :c maps to 2 which is uniquely the maximum — no tie to resolve. When genuine ties exist at the maximum, max_by returns the first pair it encountered at that value during iteration. If the specific tied pair matters to your logic, add a secondary comparison criterion inside the block itself.
Getting the top N pairs
max_by takes an optional n argument to return multiple pairs, sorted descending:
top_3 = products.max_by(3) { |_name, price| price }
# => [["Gadget", 24.99], ["Widget", 12.99], ["Doodad", 7.99]]
The top_3 call above returns the three priciest products in descending order. When you omit the block, max_by(n) compares the keys themselves using their natural ordering. For symbol-keyed hashes like the example below, this means sorting the symbols alphabetically, which is rarely what you want for anything other than string-keyed data.
Without a block, it compares keys:
{ c: 3, a: 1, b: 2 }.max_by(2)
# => [[:c, 3], [:b, 2]]
The returned value is an array of arrays when n is specified.
When you pass an integer n to max_by, Ruby returns the top n pairs sorted in descending order by the block’s return value. The single-pair form returns a two-element array, while the multi-pair form returns an array of two-element arrays. Be consistent in your destructuring pattern to avoid NoMethodError when the return type differs between the two calling conventions. The multi-pair form is especially useful for leaderboards, top-N reports, or any feature that needs more than just the single extreme value.
Symbol#to_proc shorthand
Ruby lets you skip the block when comparing a single attribute with the &: shorthand:
# Long form
products.max_by { |_name, price| price }
# Short form — exactly equivalent
products.max_by(&:last)
# => ["Gadget", 24.99]
The products.max_by(&:last) shorthand above is idiomatic Ruby that takes advantage of hash iteration yielding [key, value] arrays. Calling .last on each such array extracts the value, making max_by(&:last) equivalent to max_by { |_k, v| v }. This pattern is concise and works for any hash where you want to compare entries by their values rather than their keys.
&:last calls .last on each key-value pair. Since .last on a two-element array returns the value, this works perfectly for hashes where you want the largest value.
With empty hash
Calling max_by on an empty hash returns nil:
{}.max_by { |_k, v| v }
# => nil
When you call max_by without n on an empty hash, Ruby returns nil because there is no pair to return. Passing a positive integer changes the return type: max_by(3) on an empty hash produces an empty array rather than nil. This difference in return types is easy to miss, so code that destructures the result should explicitly check for emptiness first.
If you pass n > 0, it returns an empty array:
{}.max_by(3) { |_k, v| v }
# => []
The distinction between max_by (no n) returning nil and max_by(n) returning [] for an empty hash matters when your code destructures the result. key, value = empty_hash.max_by { ... } assigns nil to both variables, while empty_hash.max_by(1) { ... }.first would raise NoMethodError. Always guard against empty input when the result drives further computation.
Handle empty hashes explicitly if the result matters to your logic.
Default values and missing keys
max_by only iterates over keys that actually exist in the hash. If you have default values set via Hash.new(0), those are only present when accessed:
votes = Hash.new(0)
votes[:alice] += 1
votes[:bob] += 1
votes.max_by { |_k, v| v }
# => [:alice, 1] or [:bob, 1] — whichever is last in iteration order
For n > 0 with all equal values, iteration order determines which pairs are returned.
When you use Hash.new(0) to set default values, max_by only sees keys that have been explicitly stored. Default values returned by the block are not visible to iteration methods — a default is computed on access, not stored in the hash. If you need defaults to participate in max_by, explicitly store them with hash[key] = default before iterating. This distinction between stored entries and computed defaults is a general property of Ruby hashes, not specific to max_by.
Enumerable vs Hash#max
Hash also has a Hash#max method that uses natural comparison on key-value pairs:
{ c: 3, a: 1, b: 2 }.max
# => [:c, 3]
products.max
# ArgumentError: comparison of Array with Array failed
Hash#max fails when you try to compare heterogeneous values. max_by with a block never compares the pairs directly — it compares only the block’s return value, so it’s safe for any hash structure.
Finding multiple maxima
To get all keys tied for the maximum value:
scores = { alice: 98, bob: 98, carol: 95 }
scores.group_by { |_k, v| v }.max_by { |_score, group| score }.last
# => [{:alice, 98}, {:bob, 98}]
The group_by + max_by + .last chain finds all keys that share the maximum value. First, group_by clusters pairs by their score; then max_by picks the highest-scoring cluster; and .last extracts the key-value pairs from that cluster. This two-pass approach (one for grouping, one for finding the max) is still O(n) overall and is the standard pattern when ties matter.
This groups by value, then finds the highest value group, then returns all members.
Performance
max_by traverses the hash once. Complexity is O(n) where n is the hash size. This is the correct approach — sorting the hash first (O(n log n)) to find the maximum is wasteful.
See Also
- /reference/hash-methods/hash-min-by/ — the inverse: find the pair with the smallest block value
- /reference/hash-methods/has-key/ — check whether a key exists before calling
max_by - /guides/ruby-working-with-hashes/ — practical patterns for everyday hash manipulation