Skip to content
Select theme

How to use itertools for iterator helpers

Short answer: itertools supplies the adapters the standard library’s Iterator trait leaves out — unique, sorted, join, chunk_by, combinations, cartesian_product. Add the crate, bring the Itertools trait into scope, and the methods show up on every iterator you already have.

From a fresh project — cargo new iter-demo — add the block below to Cargo.toml (or run cargo add itertools). Each block after it is a complete src/main.rs; run each with cargo run. Rust 1.99.0, edition 2024, and the project name only ever shows up in cargo’s own output. Every block was run against itertools 0.15.0; itertools = "0.15" resolves to the current 0.15.x, and these APIs are stable across it.

[dependencies]
itertools = "0.15"

Dedup and sort without leaving the pipeline

Section titled “Dedup and sort without leaving the pipeline”

Vec::dedup removes consecutive repeats, and it needs a Vec to do it. unique keeps the first occurrence of each value, in the order the values arrived, straight off the iterator. sorted collects and sorts, so it chains where the slice method sort cannot. join folds the whole thing into one String.

use itertools::Itertools;
fn main() {
let hits = [3, 1, 4, 1, 5, 9, 2, 6];
println!("{:?}", hits.iter().unique().collect::<Vec<_>>());
println!("{:?}", hits.iter().sorted().collect::<Vec<_>>());
println!("{}", hits.iter().sorted().join(", "));
}
[3, 1, 4, 5, 9, 2, 6]
[1, 1, 2, 3, 4, 5, 6, 9]
1, 1, 2, 3, 4, 5, 6, 9

sorted and join build their whole result before the first item comes back — sorting and joining leave no other choice. Every adapter below stays lazy: nothing runs until something consumes it.

chunk_by groups consecutive elements that share a key, under the same consecutive-only rule as Python’s itertools.groupby. chunks(n) cuts an iterator into fixed-size pieces, and each piece is itself an iterator rather than a collected Vec.

use itertools::Itertools;
fn main() {
let words = ["apple", "avocado", "banana", "blueberry", "cherry"];
for (key, group) in &words.iter().chunk_by(|w| w.chars().next()) {
println!("{key:?}: {:?}", group.collect::<Vec<_>>());
}
for chunk in &(1..=7).chunks(3) {
println!("{:?}", chunk.collect::<Vec<_>>());
}
}
Some('a'): ["apple", "avocado"]
Some('b'): ["banana", "blueberry"]
Some('c'): ["cherry"]
[1, 2, 3]
[4, 5, 6]
[7]

The key is the first letter, as an Option<char>. Move one banana next to another banana further down the list and chunk_by reports two groups — it groups runs, not values. chunks needs the & in the loop: its IntoChunks is a value the chunk iterators borrow from, not an iterator itself.

cartesian_product walks two iterators; combinations(2) does the same within one iterator, each pair once, in order. Both take the size, so combinations(3) gives triples.

use itertools::Itertools;
fn main() {
for (size, colour) in ["S", "M", "L"].iter().cartesian_product(&["red", "blue"]) {
println!("{size} {colour}");
}
println!("{:?}", (1..4).combinations(2).collect::<Vec<_>>());
}
S red
S blue
M red
M blue
L red
L blue
[[1, 2], [1, 3], [2, 3]]

Many of the methods come as free functions too, with the iterable as the first argument — itertools::join(iter, ", "), itertools::sorted(iter) — which is what you reach for when passing one to a generic function. counts() is the counter Python has and Rust does not: it turns an iterator of values into a HashMap from value to number of occurrences.

use itertools::{Itertools, join, sorted};
fn main() {
println!("{}", join([1, 2, 1, 3], "-"));
println!("{:?}", sorted([3, 1, 2]).collect::<Vec<_>>());
let counts = ["a", "b", "a"].into_iter().counts();
println!("{} {}", counts.len(), counts["a"]);
let mut v = vec![1, 1, 2, 1];
v.dedup();
println!("{v:?}");
}
1-2-1-3
[1, 2, 3]
2 2
[1, 2, 1]

The last two lines are the comparison: counts() reports two distinct values and two "a"s, while Vec::dedup removes only consecutive repeats, so the trailing 1 survives.