rubyguides

Hash#except

hash.except(*keys) -> hash

The except method returns a new hash with one or more specified keys removed. The the source data stays unchanged, making this a non-destructive operation. This method was introduced in Ruby 3.0 as part of the collection refinements.

Use except when the starting hash has more data than the next step should see. It is common around parameters, logging, and serialization because the method describes the intent directly: keep everything except these named keys.

Signature

hash.except(*keys) → new_hash

Parameters

ParameterTypeDescription
*keysSymbol, StringOne or more keys to exclude from the returned hash. Both symbol and string keys are supported.

Return Value

Returns a fresh collection containing all key-value pairs from the original hash except those whose keys match the arguments.

Basic Usage

user = { name: 'Alice', email: 'alice@example.com', password: 'secret123', token: 'abc123' }

user.except(:password, :token)
# => {:name=>"Alice", :email=>"alice@example.com"}

The except call above strips two sensitive keys in a single expression. The method is variadic, so you can pass as many keys as needed, and it always returns a new hash without modifying the source. This makes it safe to use in view templates and API serializers where the original data must stay intact for other consumers.

The original hash stays unchanged:

user # => {:name=>"Alice", :email=>"alice@example.com", :password=>"secret123", :token=>"abc123"}

Because the source object is unchanged, except is safe to use in pipelines where another part of the code may still need the full data. That is especially helpful when you are preparing a public view of a private record. The method returns a new hash every time, so you can call it in multiple places without worrying about side effects leaking between different consumers of the same source hash. Because except is non-destructive, it fits naturally into method chains where you want to strip keys before passing data to the next step in a pipeline.

excluding multiple keys

You can exclude as many keys as you need:

config = { host: 'localhost', port: 3000, debug: true, verbose: true, secret: 'key' }

config.except(:secret, :debug)
# => {:host=>"localhost", :port=>3000, :verbose=>true}

The method accepts a variable number of key arguments, so you can pass as many keys as the situation calls for. The splat parameter in the method signature means each key is checked independently against the hash. If a key does not exist in the original hash, it is simply ignored and no error is raised.

symbol vs string keys

Symbol and string keys are treated as distinct. Excluding :id does not remove 'id':

params = { id: 1, 'id' => 'two', name: 'Bob' }

params.except(:id)
# => {"id"=>"two", :name=>"Bob"}

params.except('id')
# => {:id=>1, :name=>"Bob"}

This distinction matters in controller parameters and parsed JSON. If a key came from JSON, it may be a string. If it came from Ruby code, it may be a symbol. Match the key type you actually have. In Rails controllers, params usually contain string keys from the HTTP request, while Ruby code tends to use symbol keys, so the mismatch is common enough to be worth checking during code review. A quick way to handle this is to call symbolize_keys on the params hash before using except, which normalizes the key type and avoids the subtle bug of excluding a symbol key while a string key with the same name slips through.

common use case: filtering sensitive data

A frequent application is removing sensitive attributes before logging or passing hashes to external services:

credentials = {
  username: 'alice',
  password: 'supersecret',
  api_key: 'sk-1234567890',
  name: 'Alice Smith'
}

safe_credentials = credentials.except(:password, :api_key)
safe_credentials
# => {:username=>"alice", :name=>"Alice Smith"}

Stripping credentials before logging is one of the most practical uses of except. A single call removes every sensitive field at once, and because the method returns a new hash, the original data is still available for the actual authentication step that follows the log statement.

This pattern prevents accidental exposure of secrets in logs or error reports:

def create_user(params)
  # Remove sensitive fields before logging
  safe_params = params.except(:password, :api_key)
  logger.info("Creating user: #{safe_params}")
  
  # ... rest of method
end

The same pattern works for audit logs, background job payloads, and API responses. Removing sensitive keys before the data leaves the local method makes accidental exposure less likely.

chaining with other hash methods

except produces a new result, so you can chain it with other collection methods:

data = { a: 1, b: 2, c: 3, d: 4, secret: 'hidden' }

data.except(:secret).select { |_k, v| v > 1 }
# => {:b=>2, :c=>3, :d=>4}

Chaining is easiest to read when except comes before filters that assume the unsafe keys are already gone. That order makes the privacy step visible before the remaining data is shaped for the next operation.

See Also

  • hash#slice — Returns a new hash containing only the specified keys (the inverse operation)
  • hash#transform_keys — Transform keys in a hash using a block or hash argument
  • hash#merge — Combine multiple hashes into a new hash