Question
Remove a Key from a Hash in Ruby: delete vs reject and Returning the Remaining Hash
Question
In Ruby or Rails, how can you remove a key from a Hash and get back the remaining hash?
For example, adding a new key-value pair works like this:
{ a: 1, b: 2 }.merge!(c: 3)
# => { a: 1, b: 2, c: 3 }
Is there a similar built-in way to delete a key from a hash?
This works:
{ a: 1, b: 2 }.reject! { |k, _v| k == :a }
# => { b: 2 }
But I would expect something like:
{ a: 1, b: 2 }.delete!(:a)
# => { b: 2 }
The important part is that the return value should be the remaining hash, so it can be used inline, for example:
foo(my_hash.reject! { |k, _v| k == my_key })
Short Answer
By the end of this page, you will understand how Ruby Hash deletion methods work, especially the difference between removing a key and what value gets returned. You will learn when to use delete, reject, and non-mutating alternatives, how Rails adds helpers like except, and how to safely write one-line expressions that return the updated hash.
Concept
Ruby Hash objects have methods for both changing the original hash and creating a new hash.
The main idea behind this question is that different methods return different things:
merge!modifies the hash and returns the updated hash.deletemodifies the hash but returns the deleted value, not the remaining hash.rejectreturns a new hash with filtered entries removed.reject!modifies the original hash, but its return value has a special rule.
This matters because in real code, developers often want to chain methods or pass results directly into another method. To do that safely, you need to know exactly what each method returns.
In plain Ruby:
h = { a: 1, b: 2 }
h.delete(:a)
# => 1
The hash becomes:
{ b: 2 }
But the returned value is 1, because delete is designed to return the value that was removed.
If your goal is to get the remaining hash, you usually want one of these patterns:
- mutate then return the hash manually
- use
rejectto build a new hash - in Rails, use
exceptto return a new hash without certain keys
Understanding this distinction helps prevent bugs when chaining methods, passing values into functions, or relying on inline expressions.
Mental Model
Think of a hash like a labeled storage box.
merge!means: put a new labeled item into the box and hand me back the box.deletemeans: remove the item with this label and hand me the item that was removed.rejectmeans: build me a new box with only the items I want to keep.
So even though delete changes the box, its return value is not the box. It is the removed item.
That is why this can feel surprising at first:
h = { a: 1, b: 2 }
removed = h.delete(:a)
# removed is 1, not { b: 2 }
If you need the updated box itself, use a method that returns the box, or return h after deleting.
Syntax and Examples
Core syntax
delete
Removes a key from the original hash and returns the removed value.
h = { a: 1, b: 2 }
result = h.delete(:a)
puts result
# 1
puts h
# {:b=>2}
Return the hash after deleting
If you want the remaining hash, do this:
h = { a: 1, b: 2 }
h.delete(:a)
h
# => { a: 1, b: 2 }?
That would only work if shown after the mutation is complete in an interactive sequence. In a single expression, use tap:
h = { a: 1, b: 2 }
result = h.tap { |hash| hash.delete(:a) }
# => { b: 2 }
reject
Returns a new hash without changing the original.
h = { a: 1, b: 2 }
result = h.reject { |k, _v| k == :a }
puts result
# {:b=>2}
puts h
# {:a=>1, :b=>2}
reject!
Modifies the original hash, but returns:
- the modified hash if something changed
nilif nothing was removed
h = { a: 1, b: 2 }
result = h.reject! { |k, _v| k == :a }
# => { b: 2 }
h2 = { a: 1, b: 2 }
result2 = h2.reject! { |k, _v| k == :z }
# => nil
This nil behavior is important if you want to chain or pass the result inline.
Rails except
In Rails, except is often the cleanest option for returning a new hash without certain keys.
h = { a: 1, b: 2 }
h.except(:a)
# => { b: 2 }
This does not modify the original hash.
Beginner-friendly example
user = { name: "Ava", password: "secret", role: "admin" }
public_data = user.except(:password)
puts public_data
# {:name=>"Ava", :role=>"admin"}
This is useful when you want to remove sensitive keys before logging or returning data.
Step by Step Execution
Consider this example:
h = { a: 1, b: 2 }
result = h.tap { |hash| hash.delete(:a) }
Step by step:
- Ruby creates the hash:
h = { a: 1, b: 2 }
-
tapyieldshinto the block ashash. -
Inside the block:
hash.delete(:a)
This removes the :a key from the original hash.
-
delete(:a)returns1, the deleted value. -
But
tapignores the block's return value and returns the original object instead. -
Since the original object was mutated, the final returned value is:
{ b: 2 }
So:
result
# => { b: 2 }
h
# => { b: 2 }
Now compare with delete alone:
h = { a: 1, b: 2 }
result = h.delete(:a)
Step by step:
hstarts as{ a: 1, b: 2 }delete(:a)removes:a- The hash becomes
{ b: 2 } - The returned value is
1
So:
result
# => 1
h
# => { b: 2 }
The hash changed, but the expression result is the deleted value, not the hash.
Real World Use Cases
Removing unwanted keys before sending data
params.except(:password, :token)
Useful for API responses, logs, and serialization.
Cleaning configuration hashes
options = { timeout: 10, debug: true, internal: true }
public_options = options.reject { |k, _| k == :internal }
Useful when passing only allowed options into a library.
Filtering request parameters
In Rails controllers, developers often remove keys they do not want to process further.
safe_params = params.to_h.except("admin")
Mutating data in place for performance or clarity
If you already have a hash and want to update it directly:
session_data.delete(:temporary_token)
This is common when cleaning up state after use.
Building sanitized objects
user_data = { name: "Sam", email: "sam@example.com", password: "secret" }
user_data_without_password = user_data.except(:password)
A common pattern in services, background jobs, and presenters.
Real Codebase Usage
In real projects, developers usually choose between mutating and non-mutating approaches based on intent.
1. Non-mutating for safer code
When you do not want side effects, return a new hash.
filtered = original.except(:secret)
This is easier to reason about, especially in larger codebases.
2. Mutating when the original hash should change
payload.delete(:debug)
This is common when a hash is temporary and owned by the current method.
3. Using tap for inline mutation + return object
foo(my_hash.tap { |h| h.delete(my_key) })
This is a common Ruby idiom when you want to mutate and still return the object.
4. Guarding against missing keys
if config.key?(:cache)
config.delete(:cache)
end
Not always necessary, but useful if your logic depends on whether the key existed.
5. Filtering multiple keys
In Rails:
attrs = params.to_h.except("controller", "action")
In plain Ruby:
attrs = params.reject { |k, _| ["controller", "action"].include?(k) }
6. Validation and sanitization patterns
Developers often remove unsupported or dangerous keys before passing hashes deeper into the application.
allowed = incoming.reject { |k, _| forbidden_keys.include?(k) }
This pattern appears in service objects, command objects, and adapter layers.
Common Mistakes
Mistake 1: Expecting delete to return the updated hash
Broken expectation:
h = { a: 1, b: 2 }
result = h.delete(:a)
puts result
# expected: {:b=>2}
# actual: 1
How to avoid it:
- Use
deletewhen you need the removed value. - Use
tap,reject, orexceptwhen you need the remaining hash.
Mistake 2: Using reject! inline without handling nil
h = { a: 1, b: 2 }
foo(h.reject! { |k, _| k == :z })
This may pass nil to foo because no key matched.
How to avoid it:
foo(h.reject { |k, _| k == :z })
Or mutate safely and return the object:
foo(h.tap { |hash| hash.delete(:z) })
Mistake 3: Forgetting that reject does not mutate
h = { a: 1, b: 2 }
h.reject { |k, _| k == :a }
puts h
# still {:a=>1, :b=>2}
How to avoid it:
Assign the result:
h = h.reject { |k, _| k == :a }
Mistake 4: Using the wrong block parameters
A hash block receives both key and value.
h.reject { |k| k == :a }
This works because k receives the key-value pair in some enumerable contexts differently than beginners expect, but for hashes, the clear form is:
h.reject { |k, _v| k == :a }
This is easier to read and less confusing.
Mistake 5: Accidentally modifying shared data
def sanitize(data)
data.delete(:token)
data
end
If the caller still needs the original hash, this can cause bugs.
Safer version:
def sanitize(data)
data.except(:token)
end
Or in plain Ruby:
def sanitize(data)
data.reject { |k, _| k == :token }
end
Comparisons
| Method | Mutates original hash? | Return value | Best use case |
|---|---|---|---|
delete(:key) | Yes | Deleted value | When you need the removed value |
reject { ... } | No | New filtered hash | When you want a new hash |
reject! { ... } | Yes | Modified hash or nil | When mutating is fine and nil is acceptable |
except(:key) (Rails) | No | New hash without key | Cleanest Rails option for removing keys |
| `tap { | h | h.delete(:key) }` | Yes |
delete vs reject
h = { a: 1, b: 2 }
h.delete(:a)
# => 1
h = { a: 1, b: 2 }
h.reject { |k, _| k == :a }
# => { b: 2 }
Use delete for removing one known key.
Use reject for filtering based on a condition.
reject! vs reject
rejectis safer for chaining because it always returns a new hash.reject!can returnnilif nothing changed.
Plain Ruby vs Rails
| Environment | Recommended method |
|---|---|
| Plain Ruby | delete, reject, or tap |
| Rails / ActiveSupport | except for a clean non-mutating option |
Cheat Sheet
# Remove one key, mutate original, return deleted value
h.delete(:a)
# Remove one key, mutate original, return mutated hash
h.tap { |hash| hash.delete(:a) }
# Remove by condition, return new hash
h.reject { |k, v| k == :a }
# Remove by condition, mutate original
# returns mutated hash OR nil if nothing changed
h.reject! { |k, v| k == :a }
# Rails: return new hash without given keys
h.except(:a)
h.except(:a, :b)
Rules to remember
deletereturns the deleted value.rejectreturns a new hash.reject!may return nil.exceptis a Rails-friendly way to remove keys without mutation.- Use
tapif you want to mutate and still return the hash.
Safe inline patterns
foo(my_hash.reject { |k, _| k == my_key })
foo(my_hash.except(my_key)) # Rails
foo(my_hash.tap { |h| h.delete(my_key) })
Edge case
h = { a: 1, b: 2 }
h.reject! { |k, _| k == :z }
# => nil
Do not rely on reject! inline unless you are sure something will be removed.
FAQ
Is there a delete! method for Ruby hashes?
No. Ruby Hash has delete, but not delete!.
Why does Hash#delete return the deleted value instead of the hash?
That is how the method is designed. It is useful when you need the removed value immediately.
How do I remove a key and return the remaining hash in one line?
Use one of these:
h.reject { |k, _| k == :a }
h.except(:a) # Rails
h.tap { |hash| hash.delete(:a) }
What is the safest choice for method chaining?
Usually reject or Rails except, because they return a hash consistently.
Why can reject! return nil?
Because Ruby uses nil to indicate that no change was made.
Should I use delete or reject for one key?
- Use
deleteif you want to mutate and possibly need the removed value. - Use
rejectorexceptif you want the remaining hash as the result.
Does Rails add extra hash helpers?
Yes. ActiveSupport adds methods like except, which are very convenient for removing keys without mutating the original hash.
Mini Project
Description
Build a small Ruby utility that sanitizes user data before sending it to another part of your application. This demonstrates how to remove keys from a hash safely, how mutation affects the original object, and when returning a new hash is the better choice.
Goal
Create a method that removes sensitive keys from a hash and returns the cleaned hash.
Requirements
- Create a hash containing user data such as name, email, password, and token.
- Write one method that returns a new sanitized hash without modifying the original.
- Write another method that removes a key in place and returns the mutated hash.
- Show the difference in output between the original hash and the returned result.
- Use at least one example that removes more than one key.
Keep learning
Related questions
Add a Default Value to an Existing Column in a Rails Migration
Learn how to add or change a default value on an existing database column using Rails Active Record migrations safely.
Calling a Class Method from an Instance in Ruby
Learn how to call a class method from an instance in Ruby using self.class, with examples, pitfalls, and practical usage patterns.
Calling an Overridden Monkey-Patched Method in Ruby
Learn how to call the original method when monkey patching in Ruby, including alias_method patterns, examples, pitfalls, and practical usage.