Array#zip
What does zip do?
zip combines two or more arrays element-by-element, producing a collection of tuples. Each tuple contains one element from each input array at the same index position.
Think of it as a zipper on a jacket — it pairs teeth from two sides in order. It is that simple.
Array#zip is defined on the Array class, but the implementation lives in Enumerable#zip. Array includes Enumerable, so Array inherits it. The behaviour is identical whether you call it on an Array or any other Enumerable.
Basic Usage
Combining two arrays
numbers = [1, 2, 3]
letters = ['a', 'b', 'c']
numbers.zip(letters)
# => [[1, "a"], [2, "b"], [3, "c"]]
With a single array argument
When you zip two collections of equal length, each pair lines up cleanly at matching positions. This pairing style works well anytime you have parallel data streams that need to be joined index-by-index — think of aligning a column of labels with a column of measurements. The next example shows the same idea with three-element sequences, producing three pairs.
[10, 20, 30].zip([100, 200, 300])
# => [[10, 100], [20, 200], [30, 300]]
Without a block
Called without a block, zip returns a new array of sequences (an Enumerator if no arguments):
Calling zip without a block or with arguments changes the return type in a way worth noticing. When you omit arguments entirely, you get back an Enumerator rather than a concrete result — Ruby does this so you can chain further enumeration methods lazily. Pass at least one argument and the method returns a plain nested list straight away.
[1, 2, 3].zip # => #<Enumerator: [1, 2, 3]:zip>
[1, 2, 3].zip(['a', 'b', 'c']) # => [[1, "a"], [2, "b"], [3, "c"]]
Zipping multiple arrays
You can pass any number of sequences:
Once the two-collection case makes sense, extending zip to three or more input sequences is straightforward. Each position in the output tuple draws from the corresponding input at the same index, which makes zip a clean way to build record-like structures from separate parallel lists.
ids = [1, 2, 3]
names = ['Alice', 'Bob', 'Carol']
scores = [95, 87, 92]
ids.zip(names, scores)
# => [[1, "Alice", 95], [2, "Bob", 87], [3, "Carol", 92]]
Handling different-length arrays
When arrays have different lengths, zip stops at the shortest one. Extra elements from longer arrays are discarded, and shorter arrays fill in with nil:
Real data rarely arrives in perfectly matched lengths, and zip has a specific policy for that scenario. Rather than raising an exception or silently padding, it truncates the output to the length of the shortest input. The example below demonstrates both directions so you can see how the choice of which collection comes first changes which elements survive.
short = [1, 2]
long = ['a', 'b', 'c', 'd']
short.zip(long)
# => [[1, "a"], [2, "b"]]
# Reversing the order changes which elements survive
long.zip(short)
# => [["a", 1], ["b", 2], ["c", nil], ["d", nil]]
Filling short arrays with a default value
If you need a value other than nil, pad the shorter array first:
The default nil padding that zip applies to shorter inputs can be replaced with a value of your choice. By explicitly extending the shorter collection before calling zip, you control what fills the empty slots without changing zip’s own behaviour.
a = [1, 2, 3, 4]
b = ['x', 'y']
padded_b = b + [nil] * (a.length - b.length)
a.zip(padded_b)
# => [[1, "x"], [2, "y"], [3, nil], [4, nil]]
With a Block
Pass a block to iterate over each tuple without building the intermediate array:
The block form of zip is worth knowing because it skips the intermediate allocation entirely. When you only need to iterate over the pairs — for printing, logging, or side-effect work — passing a block gives you the same element-by-element access without the memory cost of building the full nested structure.
names = ['Alice', 'Bob', 'Carol']
scores = [95, 87, 92]
names.zip(scores) do |name, score|
puts "#{name}: #{score}"
end
# Output:
# Alice: 95
# Bob: 87
# Carol: 92
# => nil
When called with a block, zip returns nil.
Practical Examples
Merging keys and values into a hash
The most common use case — pairing keys with values:
Building a hash from parallel key and value lists is probably the single most common use of zip in production Ruby code. The to_h call on the zipped result is both readable and idiomatic. The alternative Hash[] constructor style shown next achieves the same result and appears in older codebases, though to_h reads more naturally today.
keys = [:name, :age, :city]
values = ['Alice', 30, 'Boston']
keys.zip(values).to_h
# => {:name => "Alice", :age => 30, :city => "Boston"}
Or equivalently, using Hash[]:
An indexed lookup table is another natural fit for zip. When you have a list of items and want to map each one to its position, zipping the values with a range of indices and converting to a hash gives you a direct lookup from item to position in a single expression.
Hash[keys.zip(values)]
# => {:name => "Alice", :age => 30, :city => "Boston"}
Creating an indexed list
Parallel iteration over related data is cleaner with zip than with manual index management. Instead of writing a loop that increments a counter and reaches into two separate collections, you zip them together and destructure each tuple directly in the block parameters.
items = ['apple', 'banana', 'cherry']
indexes = [0, 1, 2]
items.zip(indexes).to_h
# => {"apple" => 0, "banana" => 1, "cherry" => 2}
Pairing data for iteration
When you have parallel sequences and want to process them together:
Interleaving is a less obvious but handy trick with zip. By zipping two sequences and then flattening the result, you alternate elements from each source — useful for merging sorted halves, weaving together two ordered streams, or building a round-robin schedule.
prices = [29.99, 49.99, 9.99]
names = ['book', 'shirt', 'pen']
prices.zip(names).each do |price, name|
puts "#{name} costs $#{price}"
end
# Output:
# book costs $29.99
# shirt costs $49.99
# pen costs $9.99
Interleaving two arrays
The comparison table above highlights the structural difference, but seeing both methods applied to similar data makes the distinction concrete. The key insight is that zip starts from separate flat collections and builds tuples outward, while transpose starts from an already-nested structure and pivots it.
left = [1, 3, 5]
right = [2, 4, 6]
left.zip(right).flatten
# => [1, 2, 3, 4, 5, 6]
zip vs transpose
Both zip and transpose rearrange elements into nested sequences, but they work on different data structures:
| Method | Operates on | What it does |
|---|---|---|
zip | Multiple separate arrays | Pairs elements at the same index |
transpose | A single 2D array | Swaps rows and columns |
# zip pairs elements from separate arrays
[1, 2, 3].zip(['a', 'b', 'c'])
# => [[1, "a"], [2, "b"], [3, "c"]]
# transpose swaps rows and columns of a 2D array
matrix = [[1, 2], ['a', 'b'], [:x, :y]]
matrix.transpose
# => [[1, "a", :x], [2, "b", :y]]
Key difference: transpose requires a rectangular (consistent row length) structure and raises an error if rows differ. zip naturally handles unequal lengths by stopping at the shortest array.
Performance Notes
zip has O(n) time complexity where n is the length of the shortest array. It iterates once and creates a new array of n tuples.
Memory considerations
zipalways allocates a new array- The inner tuples are new array objects
- For large datasets, this allocation overhead adds up
When to use it
zip is ideal when you have parallel sequences that represent related data (keys/values, coordinates, name/score pairs). For quick iteration over parallel data, the block form avoids intermediate allocation:
# Block form: no intermediate array
[1, 2, 3].zip(['a', 'b', 'c']) { |n, l| puts "#{n}:#{l}" }
# No-block form: creates intermediate arrays
zipped = [1, 2, 3].zip(['a', 'b', 'c']) # => [[1, "a"], [2, "b"], [3, "c"]]
Converting to hash efficiently
When you need a hash, .to_h on the result is cleaner than Hash[] and has equivalent performance:
The block form shown above passes each pair directly to the block as it iterates, never constructing an intermediate nested structure. The version without a block below builds the full result in memory first. For large data sets where you only need to iterate, the block variant avoids unnecessary allocation and can be noticeably lighter on the garbage collector.
keys.zip(values).to_h # preferred in modern Ruby
Hash[keys.zip(values)] # older style, same result
See Also
- /reference/enumerable/enumerable-zip/ — The Enumerable version of zip (Array inherits this behaviour)
- /reference/enumerable/enumerable-map/ — Transform each element with a block
- /reference/array-methods/array-flatten/ — Flatten nested sequences
- /reference/enumerable/enumerable-reduce/ — Accumulate collection into a single value