rubyguides

Hash#reject

hash.reject { |key, value| block } -> hash

Hash#reject returns a new copy containing only the key-value pairs where the block returns falsy. It is the inverse of Hash#select — where select keeps pairs that pass the condition, reject discards them. The original collection is never modified by the non-bang version.

Syntax

hash.reject { |key, value| condition }  # => new hash
hash.reject                             # => Enumerator
hash.reject! { |key, value| condition }  # => self or nil
hash.reject!                            # => Enumerator

Both methods return an Enumerator when called without a block, so you can chain other iterators or pass the iterator to lazy. The enumerator is created lazily and evaluates each key-value pair only when consumed.

Hash#reject vs Hash#reject!

The bang/non-bang distinction follows Ruby’s standard pattern:

MethodModifies original?Returns when changedReturns when nothing removed
rejectNoNew hashNew hash (copy)
reject!Yesselfnil
h = { a: 1, b: 2, c: 3 }

new_h = h.reject { |k, v| v > 1 }
puts h      # => {:a=>1, :b=>2, :c=>3}  (unchanged)
puts new_h  # => {:a=>1}

h.reject! { |k, v| v > 1 }
puts h      # => {:a=>1}  (modified in place)

That nil return from reject! trips people up. When nothing gets removed, you get nil instead of the receiver or self. Always check for nil before using the return value of a bang method in further operations like chaining.

h = { a: 1, b: 2 }
result = h.reject! { |k, v| v > 10 }
result  # => nil  (nothing removed — returns nil, not h)
h       # => {:a=>1, :b=>2}  (unchanged)

result = h.reject! { |k, v| v > 0 }
result  # => {}  (something removed — returns self)
h       # => {}

This means you can’t safely chain reject! the way you chain reject. After h.reject! { ... } you might have nil or the now-empty collection — neither is a hash you can call hash methods on predictably. When you need to chain operations, reach for the non-bang version or guard the call with a nil check.

reject is NOT delete_if

Ruby documents delete_if as the equivalent of reject!, but they are separate C implementations, not aliases. They produce the same result, but delete_if always returns self, while reject! can return nil:

h = { a: 1, b: 2, c: 3 }

# delete_if: always returns self
h.dup.delete_if { |k, v| v > 2 }  # => {:a=>1, :b=>2}

# reject!: returns nil if nothing removed
h.dup.reject! { |k, v| v > 10 }   # => nil
h.dup.reject! { |k, v| v > 2 }    # => {:a=>1, :b=>2}

Both remove entries where the block returns truthy. The only behavioral difference is that reject! signals “no changes” with nil. This is a deliberate design choice in Ruby — the nil return follows the convention that bang methods return nil when nothing was modified.

reject vs select (and filter)

select and filter are identical — filter is the newer name. They keep pairs where the block returns truthy. reject is the exact inverse, so reject { |k,v| condition } gives you the same result as select { |k,v| !condition }.

MethodKeeps pair when block returns…Alias
select / filterTruthyYes (filter = select)
rejectFalsyNo
h = { a: 1, b: 2, c: 3, d: 4 }

h.select  { |k, v| v > 2 }  # => {:c=>3, :d=>4}
h.reject  { |k, v| v > 2 }  # => {:a=>1, :b=>2}
h.filter  { |k, v| v > 2 }  # => {:c=>3, :d=>4}  (filter == select)

reject { |k,v| condition } gives the same result as select { |k,v| !condition }. The bang variants follow the same symmetry, so you can flip between the two depending on which condition reads more naturally in the context of the calling code. Both produce the same output when inverted.

h = { a: 1, b: 2, c: 3, d: 4 }
h.select!  { |k, v| v > 2 }  # => {:c=>3, :d=>4}
h.filter!  { |k, v| v > 2 }  # => {:c=>3, :d=>4}  (same thing)
h.reject!  { |k, v| v > 2 }  # => {:a=>1, :b=>2}

select! and filter! are true aliases in the C source. reject! is its own implementation. The fact that reject! has a separate implementation is worth remembering because it explains the nil return behavior — that return value is baked into the method, not inherited from a shared ancestor. Understanding this distinction helps you choose the right method when the return value matters for further chaining.

Enumerator Form

Without a block, both return an Enumerator:

h = { a: 1, b: 2, c: 3 }

h.reject.each { |k, v| v.even? }   # => {:a=>1, :c=>3}
h.reject!.each { |k, v| v.even? } # => {:a=>1, :c=>3}

This lets you pass the Enumerator to lazy or chain other iterator methods. The enumerator form is especially useful when the filtering condition should be composed from several smaller predicates that each live in their own method. You can also pass the enumerator directly to methods that accept an Enumerable argument.

Common Patterns

Cleaning nil values from user input

input = { name: "Alice", email: "alice@example.com", phone: nil, age: nil }

input.reject { |_k, v| v.nil? }
# => {:name=>"Alice", :email=>"alice@example.com"}

Removing entries by key pattern

When you need to strip entries whose keys match a pattern, reject makes the intent clear. The block receives each key-value pair and you return true for the entries that should be dropped. This approach is cleaner than iterating and deleting manually because the logic stays in one expression.

env = { RAILS_ENV: "production", RACK_ENV: "production", DEBUG: "true", HOME: "/root" }

env.reject { |key, _| key.to_s.start_with?("RAILS") }
# => {:DEBUG=>"true", :HOME=>"/root"}

Chaining filters

Combining select and reject in a chain reads naturally because each method narrows the result. The data flows through each filter like a pipeline, keeping the code linear and easy to scan. The order of the chain determines which filter applies first, and Ruby evaluates them left to right.

data = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 }

data.select { |k, v| v > 2 }.reject { |k, v| v.even? }
# => {:c=>3, :e=>5}

Edge Cases

Empty hash: reject returns {}. reject! returns nil.

All pairs rejected: reject returns {}. reject! returns self (the now-empty collection).

Default values are not copied to the new hash:

The default value or proc attached to a receiver is not carried over when reject creates a new copy. If you later access a missing key on the returned copy, you will get nil rather than the default value from the original.

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

h.reject { |k, v| v > 10 }
h[:missing]  # => 0  (original unchanged)

The default value or proc is not copied to the returned copy by reject. The new copy has nil defaults.

See Also