How to zip two Vecs together

zip pairs two iterators item by item, stopping at the shorter side. Walk through iter vs into_iter, parallel mutation with iter_mut, and when itertools' izip! beats nested zips.

Pairing things up

You have two lists. Maybe one is product names and the other is prices. Maybe one is x-coordinates and the other is y-coordinates. Maybe one is a list of users and the other is a parallel list of last-login timestamps. You want to walk through them together: first pair, second pair, third pair. The general operation has a name in functional programming: zip. Like a zipper, it interleaves the two sides into one stream of pairs.

Rust ships zip as an iterator adapter on every iterator, so once you've turned your Vec into an iterator you're one method call away from pairs.

The minimal example

fn main() {
    let names = vec!["alice", "bob", "carol"];
    let scores = vec![95, 88, 73];

    // .iter() borrows each Vec, yielding &T.
    // .zip() takes another iterator and returns an iterator of pairs.
    // .collect() runs the chain and stuffs the pairs into a Vec.
    let paired: Vec<(&&str, &i32)> = names.iter().zip(scores.iter()).collect();

    println!("{:?}", paired);
}

The weird-looking Vec<(&&str, &i32)> is the price of borrowing twice: iter() on a Vec<&str> yields &&str. Most of the time you'll want to avoid that double borrow. Read on.

Walking through what happens

zip is lazy. Calling names.iter().zip(scores.iter()) doesn't visit anything yet. It returns a Zip<I, J> struct that holds the two source iterators. When something pulls (like collect or a for loop), the zip pulls one item from each source and yields them as a tuple. When either source runs out, the whole zip stops. That's the rule worth remembering: zip ends with the shorter side.

fn main() {
    let a = vec![1, 2, 3, 4, 5];
    let b = vec![10, 20, 30];

    // Stops after 3 pairs because b runs out first.
    for (x, y) in a.iter().zip(b.iter()) {
        println!("{x} + {y} = {}", x + y);
    }
}

No error, no panic. Items 4 and 5 from a are simply ignored. Sometimes that's what you want; sometimes it isn't. If you want to know when the lengths differ, check before zipping with assert_eq!(a.len(), b.len());.

Picking the right iter() flavor

This is the part that trips people up most. There are three iterator constructors on Vec<T>, and the choice changes what zip yields.

fn main() {
    let a = vec!["alice", "bob"];
    let b = vec![10, 20];

    // iter() borrows. Yields &T. zip yields (&T, &U).
    for (name, score) in a.iter().zip(b.iter()) {
        // name: &&str, score: &i32
        println!("{name}: {score}");
    }

    // into_iter() consumes. Yields T. zip yields (T, U).
    // After this, a and b are gone.
    for (name, score) in a.into_iter().zip(b.into_iter()) {
        // name: &str, score: i32 — owned, no extra references
        println!("{name}: {score}");
    }
}

For most pair-and-print use cases, iter() is fine. If you need owned values (to push into another collection, to send to a function that takes by value, to mutate), reach for into_iter().

There's also iter_mut() for parallel mutation:

fn main() {
    let mut a = vec![1, 2, 3];
    let b = vec![10, 20, 30];

    // iter_mut on the left side yields &mut i32.
    // iter on the right side yields &i32.
    // We mutate a in place using values from b.
    for (x, y) in a.iter_mut().zip(b.iter()) {
        *x += y;
    }

    println!("{:?}", a);   // [11, 22, 33]
}

*x += y looks busy until you remember x is &mut i32 and y is &i32. The * dereferences x so we can write through it; y autoderefs in arithmetic. Keep that in mind and parallel mutation is straightforward.

A more realistic example: building a HashMap from two parallel Vecs

When you import data, it's common to receive headers and values in two slices. Here's how to turn them into a HashMap.

use std::collections::HashMap;

fn main() {
    let headers = vec!["name", "city", "age"];
    let values  = vec!["Mira", "Lisbon", "31"];

    // collect on a HashMap consumes (K, V) tuples. zip produces them.
    // We use into_iter so the items are owned, not references.
    let row: HashMap<&str, &str> = headers.into_iter().zip(values.into_iter()).collect();

    println!("{:?}", row);
}

If the lengths differ, the longer side gets silently truncated. If your input is supposed to have matching lengths and a mismatch indicates corruption, assert that before zipping.

Iterating with index using zip

enumerate is what you usually reach for, but zip lets you do the same with any range:

fn main() {
    let names = vec!["alice", "bob", "carol"];

    // (1..) is an infinite iterator. zip stops when names ends, so it's safe.
    for (i, name) in (1..).zip(names.iter()) {
        println!("{i}. {name}");
    }
}

That "infinite iterator on the left, finite on the right" pattern is one of the cleaner uses of zip. The shorter side controls the length, so the infinite range never runs to the end.

Zipping more than two iterators

You can chain zips, but the result becomes nested tuples and gets ugly fast:

let triples: Vec<(i32, char, &str)> = a.iter()
    .zip(b.iter())
    .zip(c.iter())
    .map(|((x, y), z)| (*x, *y, *z))
    .collect();

For three-way zips, the itertools crate has izip!:

use itertools::izip;

for (x, y, z) in izip!(&a, &b, &c) {
    // ...
}

If you find yourself reaching for triple-zip more than a few times, itertools is worth the dependency.

Common pitfalls

Assuming zip pads the shorter side. It doesn't. The output stops at the shorter input. If you need padding, use zip_longest from itertools or pad one side first with chain.

Misreading the type after iter().zip(iter()). The pair is (&T, &U), so destructuring with for (x, y) in ... gives you references. If you do arithmetic, the operators autoderef. If you call methods, autoderef usually saves you. If you want owned values, use into_iter().

Forgetting that zip is lazy. a.iter().zip(b.iter()) is just a description. Nothing happens until you collect or iterate.

Using zip when you wanted alternation. Zip pairs the items; if you want them in alternating order (a0, b0, a1, b1, ...), use interleave from itertools instead.

When to reach for zip vs alternatives

Reach for zip when two sequences have a positional relationship: parallel arrays, coordinates, before/after pairs.

Reach for enumerate when one of the two is an index. Cleaner than zipping with 0...

Reach for iter().chain(other.iter()) when you want to glue two sequences into one long sequence, not pair them.

If the relationship is keyed, not positional, use a HashMap. Building a HashMap by zipping two parallel Vecs is a common bridge between flat input and keyed lookup.

If you only need a particular pair (the third one, say), zip and nth(2) rather than collecting first.

Where to go next

How to sort a Vec

How to split Vec into chunks

How to Filter a Vector in Rust