Hash#transform_values
hash.transform_values { |value| block } -> hash Hash#transform_values creates a new hash by transforming each value using a block while keeping keys unchanged. It’s the counterpart to Hash#transform_keys when you need to modify values rather than keys.
Use it when the keys already describe the data correctly and only the stored values need cleanup from the key-value pairs. That happens often with parameters, API responses, configuration maps, and small lookup tables. The method keeps the shape of the hash stable, which makes the transformation easier to review than rebuilding key-value pairs by hand.
Syntax
hash.transform_values { |value| block }
The block receives each value and should return the new value to use.
Basic Usage
Simple value transformation
The block receives each value one at a time and must return the replacement value. The method builds a new collection behind the scenes, so the original mapping stays unchanged. This non-destructive behavior makes transform_values safe to call on shared configuration objects without worrying about side effects.
scores = { alice: 95, bob: 82, carol: 78 }
scores.transform_values { |score| score + 5 }
# => { alice: 100, bob: 87, carol: 83 }
Convert values to strings
Adding a fixed amount to every value is a common pattern in score adjustments and grade curves. The block receives the numeric value and returns the adjusted result, while the keys pass through untouched. This keeps the focus on the data transformation rather than on bookkeeping the key structure.
prices = { apple: 100, banana: 200, orange: 150 }
prices.transform_values(&:to_s)
# => { "apple" => "100", "banana" => "200", "orange" => "150" }
Type conversion
Converting values to strings with the shorthand &:to_s is a concise way to normalize a set of values for display or serialization. The method works with any callable that accepts a single argument, so you can pass a proc, a lambda, or a method reference using the ampersand syntax.
data = { count: "42", ratio: "3.14", flag: "true" }
data.transform_values { |v| eval(v) }
# => { count: 42, ratio: 3.14, flag: true }
Practical Examples
Normalize data
Type conversion with transform_values is useful when reading data from external sources where everything arrives as a string. Each value passes through the conversion block independently, so you can apply different coercion strategies by checking the value’s type or the key’s name inside the block.
input = { name: " ALICE ", city: "london", status: "ACTIVE" }
input.transform_values { |v| v.strip.downcase }
# => { name: "alice", city: "london", status: "active" }
Apply calculations
Normalizing text values by stripping whitespace and lowercasing is a typical preprocessing step before validation or storage. The block chains multiple string methods together, and each value receives the same treatment regardless of which key it belongs to. This uniformity makes the cleanup easy to audit.
products = { laptop: 999, mouse: 29, keyboard: 79 }
products.transform_values { |price| (price * 0.9).round }
# => { laptop: 899, mouse: 26, keyboard: 71 }
Sanitize user input
Applying a discount to product prices with transform_values reads clearly because the block focuses on the computation at hand. The rounding step after multiplication avoids floating-point display artifacts that could confuse users when prices are shown in the user interface.
fields = { username: " alice ", bio: "Hello\nWorld", website: "HTTP://EXAMPLE.COM" }
fields.transform_values do |value|
value.to_s.strip.gsub(/^https?:\/\//i, "").downcase
end
# => { username: "alice", bio: "Hello\nWorld", website: "example.com" }
comparison with related methods
transform_values vs map
Using transform_values is more idiomatic than using map to transform values:
Sanitizing user-submitted fields often requires more complex logic than a one-liner. Using a multi-line block with transform_values keeps each cleanup step visible: trimming, normalizing the URL scheme, and lowercasing. The block scope isolates the transformation from the rest of the application logic.
hash = { a: 1, b: 2, c: 3 }
# Less idiomatic
hash.map { |k, v| [k, v * 10] }.to_h
# => { a: 10, b: 20, c: 30 }
# More idiomatic
hash.transform_values { |v| v * 10 }
# => { a: 10, b: 20, c: 30 }
transform_values vs transform_values!
The bang version transform_values! modifies the hash in place:
The map plus to_h approach works but adds an intermediate array allocation. transform_values communicates intent directly — the method name tells the reader exactly what happens. This clarity matters in code review, where an unfamiliar pattern might raise questions that a well-named method would not.
config = { timeout: 30, retries: 5 }
config.transform_values! { |v| v * 2 }
# config is now { timeout: 60, retries: 10 }
Use transform_values when you want a non-destructive operation that returns a fresh collection.
transform_values vs transform_keys
Transform values operates on values while keeping keys the same:
The bang version modifies the original data structure in place, which can be convenient but also risky. When multiple parts of a program hold a reference to the same collection, a transform_values! call in one place silently changes the data seen everywhere else. This is a common source of bugs in shared configuration or caching layers.
data = { name: "Alice", age: 30 }
data.transform_keys(&:to_s)
# => { "name" => "Alice", "age" => 30 }
data.transform_values(&:to_s)
# => { name: "30", age: "30" }
chaining with other methods
Combine with transform_keys
Comparing transform_values and transform_keys reveals the symmetry of the two methods. One operates on values while preserving keys, the other does the opposite. Together they provide a complete toolkit for reshaping the contents of a collection without manual iteration over each entry.
input = { FIRST_NAME: "alice", LAST_NAME: "smith", AGE: 30 }
input
.transform_keys(&:downcase)
.transform_values { |v| v.is_a?(String) ? v.capitalize : v }
# => { first_name: "Alice", last_name: "Smith", age: 30 }
Combine with select
Combining transform_keys and transform_values in a chain normalizes both dimensions of the data in a single pipeline. Each step has a single responsibility, so the reader can trace the transformation from raw input to clean output without unpacking nested logic. The intermediate results are clear at each stage.
users = { alice: { active: true, score: 95 }, bob: { active: false, score: 82 } }
users.transform_values { |v| v[:score] if v[:active] }.compact
# => { alice: 95 }
Combine with merge
Chaining transform_values with select or compact lets you both reshape and filter data in one expression. The order matters: transform first to normalize the values, then filter based on the transformed result. This two-phase approach is easier to debug than a single block that tries to do everything at once.
defaults = { debug: false, timeout: 30 }
defaults.transform_values(&:to_s).merge({ "debug" => "true", "timeout" => "60" })
# => { "debug" => "true", "timeout" => "60" }
These chains work best when each step has one clear job. For example, normalize keys first, transform the values second, and merge defaults last. Keeping that order predictable makes the hash easier to inspect in a debugger and easier to test with a small input.
in-place modification with transform_values!
If you need to modify the original hash instead of creating a new one, use transform_values!:
cache = { user_1: "cached_data", user_2: "more_cached" }
cache.transform_values!(&:upcase)
# cache is now { user_1: "CACHED_DATA", user_2: "MORE_CACHED" }
This is useful when working with configuration objects that you want to modify directly.
Be careful with the bang form when the same dictionary is shared across several callers. Updating in place can be efficient, but it also means every reference sees the changed values. Use the non-bang form when a caller may still need the original copy.
Edge Cases
Empty hash
{}.transform_values { |v| v.to_s }
# => {}
Mixed value types
Chaining with merge lets you combine transformed defaults with overrides in a single expression. The transform_values call normalizes the defaults, and then merge layers the overrides on top. This pattern is common in configuration assembly where you need type consistency across default and user-supplied values.
mixed = { a: 1, b: "two", c: nil }
mixed.transform_values { |v| v.to_s }
# => { a: "1", b: "two", c: "" }
Block not provided
Without a block, transform_values returns an enumerator:
The bang form exists for performance-sensitive code where allocating a new collection is wasteful. When the original data is no longer needed after the transformation, modifying it in place saves memory and avoids garbage collection pressure. Just make sure no other code path still expects the pre-transformation values.
hash = { a: 1, b: 2, c: 3 }
hash.transform_values.map { |v| v * 10 }
# => [10, 20, 30]
Ruby version notes
- Ruby 2.5: Introduced
transform_values - Ruby 2.5: Introduced
transform_values! - Ruby 2.5: Added blockless form returning enumerator
See Also
- Hash#transform_keys — Transform hash keys
- Hash#values — Get all hash values as an array
- Hash#merge — Merge hashes together