How to zip iterators

Use Iterator::zip for two iterators or itertools::izip for multiple to combine items into tuples.

When data flows in parallel

You're parsing a CSV file. One column holds names, another holds ages. You need to print "Name is Age" for every row. Or you're building a game and have a list of sprite coordinates and a list of sprite types. You want to iterate over both at the same time to draw the screen. This happens constantly. You have parallel streams of data, and you need to walk them side-by-side.

In Python or JavaScript, you'd reach for a zip function. Rust has the same tool, built right into the standard library. The behavior is slightly stricter, which saves you from silent bugs. You pair elements from two iterators, and the result is a single iterator of tuples. The first element from the left locks with the first from the right. The second with the second.

How zip works

Zipping takes two iterators and pairs up their elements. The result is an iterator that yields (T, U) tuples, where T comes from the first iterator and U from the second.

Think of a zipper on a jacket. Two rows of teeth slide together. Each tooth on the left locks with a tooth on the right. If one row runs out of teeth before the other, the zipper stops. The remaining teeth stay unpaired. Rust follows the same rule. The shorter iterator wins. The longer one gets truncated silently. This is a deliberate design choice. It prevents you from accidentally reading past the end of a buffer while assuming the data matches.

The minimal example

The standard library provides Iterator::zip. It works on any iterator, including slices, vectors, and custom types.

fn main() {
    let numbers = vec![1, 2, 3];
    let letters = vec!['a', 'b'];

    // Zip pairs elements. The shorter iterator stops the process.
    // Here, letters has length 2, so the loop runs twice.
    let zipped = numbers.iter().zip(letters.iter());

    for (num, letter) in zipped {
        // Prints: 1, a
        // Prints: 2, b
        // The value 3 is never visited.
        println!("{}, {}", num, letter);
    }
}

The compiler knows the types. numbers.iter() yields &i32. letters.iter() yields &char. The zipped iterator yields (&i32, &char). You get references by default. If you need owned values, call .into_iter() instead, but that moves the vectors and consumes them.

Convention aside: write a.iter().zip(b.iter()) explicitly. Some developers write a.iter().zip(b) when b is a slice, relying on auto-deref. The explicit form is clearer. It signals that you are borrowing from both sides intentionally.

The shorter iterator always wins. If you need the longer one, you're using the wrong tool.

Lazy evaluation and zero cost

zip returns an iterator, not a vector. It is lazy. Nothing happens until you consume the iterator with a loop, .collect(), or another combinator like .map(). This means zip allocates no memory. It creates a small struct on the stack that holds references to the two underlying iterators.

When you iterate, the compiler inlines the logic. The resulting machine code is identical to a hand-written loop with an index. You get the safety of a method chain with the performance of raw indexing. There is no overhead.

If you try to zip owned values without iterators, the compiler rejects you with E0382 (use of moved value). zip consumes the iterators it receives. You must provide iterators via .iter(), .iter_mut(), or .into_iter().

Trust the optimizer. The iterator chain compiles to a tight loop.

Mutability in the mix

Zipping shines when you need to update data in place. You can zip a mutable iterator with an immutable one. This lets you modify one stream based on the other without creating temporary copies.

fn main() {
    let mut buffer = vec![0, 0, 0];
    let config = vec![10, 20, 30];

    // Zip mutable refs with immutable refs.
    // This allows updating buffer in place using config values.
    for (buf_val, cfg_val) in buffer.iter_mut().zip(config.iter()) {
        *buf_val = cfg_val * 2;
    }

    println!("{:?}", buffer); // [20, 40, 60]
}

This pattern is idiomatic for element-wise updates. You avoid allocating a new vector for the result. The borrow checker ensures you don't alias the mutable reference. You can read from config and write to buffer safely because they are distinct allocations.

Mutability and immutability can coexist in a zip. Use this to update data in place without temporary copies.

Beyond two iterators

Real code often needs three or four streams. You have x-coordinates, y-coordinates, and colors. Chaining zip creates nested tuples. a.iter().zip(b.iter()).zip(c.iter()) yields ((x, y), z). You have to unpack nested tuples. It gets messy fast.

fn main() {
    let x = vec![0.0, 1.0, 2.0];
    let y = vec![0.0, 1.0, 2.0];
    let color = vec!["red", "green", "blue"];

    // Chaining zip creates nested tuples: ((x, y), color)
    // Unpacking requires double parentheses.
    let nested = x.iter().zip(y.iter()).zip(color.iter());

    for ((x_val, y_val), col) in nested {
        println!("({}, {}) is {}", x_val, y_val, col);
    }
}

The itertools crate solves this with izip!. It zips arbitrary numbers of iterators into a flat tuple. It is a macro that expands to efficient code. The community considers itertools the standard extension for iterator utilities. If you need advanced iterator tricks, you are likely using itertools.

use itertools::izip;

fn main() {
    let x = vec![0.0, 1.0, 2.0];
    let y = vec![0.0, 1.0, 2.0];
    let color = vec!["red", "green", "blue"];

    // izip! produces a flat tuple: (x, y, color)
    // Avoids nested tuple unpacking.
    for (x_val, y_val, col) in izip!(x, y, color) {
        println!("({}, {}) is {}", x_val, y_val, col);
    }
}

Convention note: izip! takes expressions, not just variables. You can write izip!(a.iter(), b.iter().skip(1), c). It handles the borrowing and typing automatically.

Don't nest tuples manually. Reach for izip! when you hit three streams.

Pitfalls and safety

The biggest trap is assuming iterators are the same length. zip truncates silently. If one list has 100 items and the other has 99, you lose one item without a warning. This is a runtime logic error, not a compile error. In data processing, this can corrupt results silently.

If your data must match, use itertools::zip_eq. It behaves like zip but panics at runtime if the lengths differ. This turns a silent data loss into a loud failure. It is better to crash now than to produce wrong output.

use itertools::Itertools;

fn main() {
    let a = vec![1, 2, 3];
    let b = vec!['a', 'b']; // Missing 'c'

    // zip_eq panics if lengths differ.
    // This is safer for data integrity.
    for (num, letter) in a.iter().zip_eq(b.iter()) {
        println!("{}, {}", num, letter);
    }
    // Panics: iterators have different lengths
}

Sometimes you need to process all elements even if iterators have different lengths. Use itertools::zip_longest. It fills the shorter side with None. You get an iterator of (Option<T>, Option<U>).

use itertools::Itertools;

fn main() {
    let a = vec![1, 2];
    let b = vec!['a', 'b', 'c'];

    // zip_longest fills the shorter side with None.
    // You must handle the Option variants.
    for (opt_num, opt_char) in a.iter().zip_longest(b.iter()) {
        match (opt_num, opt_char) {
            (Some(n), Some(c)) => println!("{}, {}", n, c),
            (Some(n), None) => println!("{}, <missing>", n),
            (None, Some(c)) => println!("<missing>, {}", c),
            _ => unreachable!(),
        }
    }
}

If your data must match, use zip_eq. Let the program crash now rather than corrupt data later.

Decision matrix

Use Iterator::zip when you have exactly two iterators and want zero dependencies. Use itertools::izip! when you need to zip three or more iterators into a flat tuple. Use itertools::zip_eq when the iterators must have the same length and a mismatch indicates a bug. Use itertools::zip_longest when you need to process all elements even if iterators have different lengths, filling the shorter side with None.

Where to go next