Question

Remove a Key from a Hash in Ruby: delete vs reject and Returning the Remaining Hash

rubyruby-on-railsruby-on-rails-3hashmapruby-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.
  • delete modifies the hash but returns the deleted value, not the remaining hash.
  • reject returns 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 reject to build a new hash
  • in Rails, use except to 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.
  • delete means: remove the item with this label and hand me the item that was removed.
  • reject means: 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.

Take Quiz

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
  • nil if 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:

  1. Ruby creates the hash:
h = { a: 1, b: 2 }
  1. tap yields h into the block as hash.

  2. Inside the block:

hash.delete(:a)

This removes the :a key from the original hash.

  1. delete(:a) returns 1, the deleted value.

  2. But tap ignores the block's return value and returns the original object instead.

  3. 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:

  1. h starts as { a: 1, b: 2 }
  2. delete(:a) removes :a
  3. The hash becomes { b: 2 }
  4. 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.

Take Quiz

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 delete when you need the removed value.
  • Use tap, reject, or except when 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

MethodMutates original hash?Return valueBest use case
delete(:key)YesDeleted valueWhen you need the removed value
reject { ... }NoNew filtered hashWhen you want a new hash
reject! { ... }YesModified hash or nilWhen mutating is fine and nil is acceptable
except(:key) (Rails)NoNew hash without keyCleanest Rails option for removing keys
`tap {hh.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

  • reject is safer for chaining because it always returns a new hash.
  • reject! can return nil if nothing changed.

Plain Ruby vs Rails

EnvironmentRecommended method
Plain Rubydelete, reject, or tap
Rails / ActiveSupportexcept for a clean non-mutating option
Take Quiz

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

  • delete returns the deleted value.
  • reject returns a new hash.
  • reject! may return nil.
  • except is a Rails-friendly way to remove keys without mutation.
  • Use tap if 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 delete if you want to mutate and possibly need the removed value.
  • Use reject or except if 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.

Take Quiz

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.
Take Quiz

Keep learning

Related questions

View more