rubyguides

Hash#delete

delete(key)

Removes the key-value pair for the given key from the collection and returns the associated value.

This is useful when you want to take a value out of a hash and keep the removed item at the same time. The method is straightforward, but that return value makes it handy for cleanup and extraction tasks.

Because delete changes the collection as it goes, it is most useful when removal is part of the job itself. That keeps the code honest about its side effects and makes the return value useful for the next step.

scores = { alice: 95, bob: 87, carol: 92 }

deleted_value = scores.delete(:bob)
# => 87

scores
# => { alice: 95, carol: 92 }

When the key is missing from the hash, delete returns nil by default. That makes it important to check the return value or use the block form when nil could also be a valid stored value.

If key is not found, delete returns nil:

scores = { alice: 95, bob: 87 }

scores.delete(:unknown)
# => nil

Instead of relying on a nil return to signal a missing key, you can pass a block to delete. The block runs only when the key is absent, and its return value replaces the default nil.

With an ifnone block

Pass a block to delete and it will be called (with no arguments) when the key is not found. The block’s return value becomes the return value of delete instead of nil:

scores = { alice: 95, bob: 87 }

deleted = scores.delete(:unknown) { |k| "Key '#{k}' not found" }
# => "Key 'unknown' not found"

This guard pattern is a small but practical way to make missing keys explicit. Instead of checking the structure in two steps, you can remove the entry and define the fallback behavior in one place.

Keeping the failure path local to the call site, which is easier to scan in small scripts and larger application code alike. When the fallback is rare, a block keeps the normal path simple and the special case obvious.

The block receives the key as an argument, so you can interpolate it into the error message:

h = { a: 1, b: 2 }
result = h.delete(:z) { |key| raise KeyError, "Missing key: #{key}" }
# => raises KeyError: "Missing key: z"

Passing a block to delete is useful when the missing key needs a fallback value, an error message, or a different default. The block runs only when the key is absent, so the normal path stays simple and the exceptional case is handled locally.

Common patterns

Rejecting nil values

delete is often used after fetching a value to ensure it’s not nil:

config = { debug: false, env: "production", port: nil }

port = config.delete(:port)  # Returns nil, not 3000 as default
# => nil

Using delete this way is helpful when a value should be consumed exactly once. Deleting removes the key from the collection and gives you the extracted value for later use if it exists.

The same pattern surfaces when a hash is acting like a temporary store. Once the key has been removed, the remaining entries are easier to reason about because the consumed value is gone from the structure.

Conditional removal with a guard

Use delete in a conditional when you only want to remove an entry if it matches a condition:

users = { alice: "admin", bob: "editor", carol: "viewer" }

if users[:bob] == "editor"
  users.delete(:bob)
end
# => "editor"

users
# => { alice: "admin", carol: "viewer" }

This guard keeps the removal tied to a clear condition, which is easier to read than deleting first and then checking the result. It also avoids mutating the hash unless the role matches the rule you expect.

Destructuring with slice

To atomically extract a subset of keys and remove them from the hash:

data = { name: "Alice", age: 30, city: "London", email: "alice@example.com" }

extracted = data.slice!(:name, :email)
# => { name: "Alice", email: "alice@example.com" }

data
# => { age: 30, city: "London" }

Note: slice! is more idiomatic for extracting multiple keys at once.

The distinction matters because delete is best when you already know the single key you want to remove. If you need a subset of entries, slice! describes the intent more directly.

Edge cases

  • Deleting from a frozen hash raises FrozenError:
    h = { a: 1 }.freeze
    h.delete(:a)
    # => FrozenError (can't modify frozen Hash)

Attempting to mutate a frozen hash will raise an error immediately, so defensive code should check the frozen state before calling delete when the hash may come from an untrusted source.

  • Deleting a key that doesn’t exist returns nil (or the block result if a block was given):
    {}.delete(:missing)
    # => nil

The nil return for missing keys is deliberate, but be careful when nil is also a valid stored value. In that case, the block form is the safer choice because it distinguishes between “found and nil” and “not found.”

  • The returned value is the original value before deletion, not the key:
    h = { key: { nested: "value" } }
    deleted = h.delete(:key)
    deleted[:nested] = "modified"  # This mutates the deleted object
    # => { nested: "modified" }

Performance

Hash#delete is an O(1) operation on average for hash buckets, but in the worst case (hash collisions) it can degrade to O(n) where n is the number of entries. In practice, Ruby’s hash implementation is highly optimized and deletion is very fast.

For normal application code, that performance is usually good enough that readability should drive the choice. A direct delete call is often clearer than a longer manual removal path.

See Also

  • Hash#reject — returns a new hash with entries removed (non-mutating)
  • Hash#slice — extract a subset of keys without mutating the original
  • Hash#except — remove specific keys, returning a new hash