rubyguides

Hash#compact

Synopsis

hash.compact  # => new hash without nil values
hash.compact! # => self or nil

Description

Hash#compact returns a new hash with all entries whose values are nil removed. The original hash is unchanged.

Hash#compact! removes nil values in place. It returns self if at least one entry was removed, or nil if nothing changed.

Watch out: compact! returns nil when no changes are made, not self. This catches many developers off guard — it’s the one gotcha with these methods.

Both methods take no arguments and accept no block.

Parameters

None.

Return Value

MethodReturn value
compactA new Hash with nil values removed
compact!self if changes were made, nil otherwise

what gets removed

Only nil values are removed. The value false is preserved.

h = { a: false, b: nil, c: 0 }
h.compact
# => { a: false, c: 0 }

The value false is distinct from nil in Ruby, and compact respects that distinction. This is important in practice because false is a meaningful value in many contexts: feature flags, boolean settings, and validation results all rely on false being a real datum rather than an absence. Methods like compact that only target nil let you clean up missing data without accidentally discarding legitimate boolean entries or other falsey-but-meaningful values.

Behavior Details

A critical difference between these two methods lies in what compact! returns when no nil values exist. Unlike most bang methods that return self regardless of whether changes occurred, compact! returns nil when the hash already contains no nil values.

This breaks the common ||= pattern:

config = { a: 1, b: nil }
config.compact! || config  # This works fine on first call

config = { a: 1, b: 2 }
config.compact! || config  # Returns nil instead of self — surprising!
# compact! returned nil, so `|| config` kicks in and returns the original hash
# which is correct, but the behavior is inconsistent with bang method conventions

That inconsistency is the main reason to read the return value carefully before chaining compact! in conditional expressions. If you want a stable object reference after cleanup, compact is often the safer choice because it always returns a hash regardless of input.

Edge Cases

empty hash

{}.compact
# => {}

{}.compact!
# => nil

An empty collection has no entries to compact, so the non-bang form returns another empty collection and the bang form returns nil to signal that no changes were made. This is consistent with the general behavior of compact!: it returns nil whenever the original collection already contains no nil values, regardless of whether that collection is empty or populated.

no nil values present

{ a: 1, b: 2 }.compact
# => { a: 1, b: 2 }

{ a: 1, b: 2 }.compact!
# => nil

When no values are nil, the non-bang compact returns a copy that is identical to the original. The bang form, however, returns nil, which can be surprising in method chains that expect a hash back. This is the same pattern seen in other Ruby bang methods like uniq! and select!, where a nil return signals that the operation would have been a no-op.

all values are nil

{ a: nil, b: nil }.compact
# => {}

{ a: nil, b: nil }.compact!
# => {}

When every value in the hash is nil, compact returns an empty hash, and compact! returns an empty hash as well (not nil) because changes were actually made. This is an important distinction from the case where the hash already has no nil values and compact! returns nil to signal a no-op.

default values

If a collection has a default value (set via Hash.new(0) or default=), that default is preserved and unaffected by compact:

h = Hash.new(0)
h[:missing]  # => 0
h.compact    # => {}
# The default value of 0 remains set on the original hash

The returned hash contains only stored entries, so the default value does not become part of the compacted result. That is useful when the default is there for lookup convenience, not as a real entry you want to keep.

default proc

A hash with a default proc (set via Hash.new { |h, k| h[k] = [] }) behaves differently. After calling compact, the returned hash has no default proc — it behaves like a plain hash. The original hash’s default proc is left untouched.

h = Hash.new { |h, k| h[k] = [] }
h[:a] << 1
h[:b] << 2
h.default_proc  # => #<Proc:...>

h.compact
# => {}

h.compact.default_proc  # => nil — it's gone
h.default_proc          # => #<Proc:...> — original unchanged

This difference matters if you rely on a hash to auto-build nested values. compact strips nil-valued entries but does not preserve the special behavior on the copy, so you should reapply the default proc if you need it afterward.

examples

Basic usage

user = { name: "Alice", email: nil, age: 30 }
user.compact
# => { name: "Alice", age: 30 }

user
# => { name: "Alice", email: nil, age: 30 }  # unchanged

The original hash remains available for later checks or other transformations. This is one of the main reasons many codebases prefer compact over manual delete_if calls when they only want to remove missing data without altering the source. The non-destructive behavior also makes compact safe to use inside methods that receive a hash as an argument and should not mutate the caller’s data.

Destructive in-place removal

data = { x: 10, y: nil, z: nil }
data.compact!
data
# => { x: 10 }

The destructive form is helpful when you want to mutate a temporary hash before handing it off to another method. It keeps the call site small while still making the cleanup step explicit. Just remember that compact! returns nil when nothing was removed, so do not chain method calls directly after it without checking the return value first.

Chaining

{ a: nil, b: 1, c: nil, d: 2 }.compact.values
# => [1, 2]

Using compact in a pipeline keeps the data flow clean because you never need an intermediate variable to hold the cleaned result. The method fits naturally between data-loading and data-processing steps, removing entries you do not need before the rest of the logic runs. Because it always returns a new object, you can safely chain it without worrying about side effects on the original collection.

When you are cleaning up data from an API response or a CSV import, compact gives you a one-liner that removes missing entries without altering the original source. Because the method only targets nil, it preserves false and zero values that carry real meaning. Pair it with transform_values or select when you need to clean and reshape in a single pipeline. Because nil entries often come from optional fields in JSON or YAML data, compact serves as a natural boundary between parsing and processing: strip the absent values first, then work with only the data that is actually present. This cleanup step is especially valuable when you are preparing data for serialization, since omitting nil-valued keys produces cleaner output. It also reduces the surface area for bugs by ensuring downstream code never has to handle unexpected nil values in what should be a populated data structure. The resulting collection is smaller, simpler to iterate, and less likely to trigger unexpected nil-related errors downstream.

See Also