How to iterate over HashMap

Iterate over a Rust HashMap using a for loop with the iter() method to access key-value pairs.

The leaderboard problem

You're building a leaderboard for a local arcade game. The high scores are stored in a HashMap<String, u32>. The game ends, and you need to print the results to the screen. You grab the map, but now you need to walk through every entry. In Python, you'd call .items(). In JavaScript, you'd use Object.entries(). Rust gives you a few ways to do this, and picking the right one saves allocations and keeps the borrow checker happy.

The core challenge isn't the loop syntax. It's ownership. A HashMap owns its keys and values. When you iterate, you have to decide whether you want to read the data, modify it in place, or take ownership of the contents. Rust forces you to make that decision explicit. The compiler won't let you accidentally clone a megabyte of data when you meant to borrow a reference.

References, not copies

A HashMap is like a filing cabinet where the drawers are scattered randomly based on the label, not alphabetical order. Iterating means opening every drawer and reading the card inside. The key insight in Rust is that you usually don't want to move the cards out of the cabinet while you're reading them. You want references.

The iter() method hands you references to the keys and values. It's cheap, fast, and keeps the data safe. The iterator yields (&K, &V) tuples. You get shared references. You can read the data, pass the references to other functions, or format them for output. The map stays intact.

If you need to modify the values, you use iter_mut(). This yields (&K, &mut V). You get a mutable reference to the value, but the key remains immutable. You can update a score or mutate a struct field, but you cannot change the key. Changing the key would require rehashing, which breaks the iteration invariants.

The minimal loop

Here is the standard pattern for reading a map. The code creates a map, inserts some scores, and prints them.

use std::collections::HashMap;

/// Print all entries in the map without taking ownership.
fn print_scores(scores: &HashMap<String, u32>) {
    // scores.iter() yields (&K, &V) references.
    // This avoids cloning keys or values.
    for (name, score) in scores.iter() {
        println!("{name}: {score}");
    }
}

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Alice"), 100);
    scores.insert(String::from("Bob"), 250);

    print_scores(&scores);
}

The for loop destructures the tuple into name and score. Both are references. name is &String, which coerces to &str for printing. score is &u32. The println! macro handles the dereferencing automatically.

Convention aside: You can write for (k, v) in &scores and it works exactly the same. The compiler auto-calls .iter() for you. Many Rustaceans prefer the explicit scores.iter() in library code to signal intent, but &scores is idiomatic in quick scripts and main. Pick the one that reads best in context.

How the iterator works

When you call .iter(), the HashMap doesn't copy anything. It creates an iterator struct that holds a reference to the map and an index. Each call to next() on the iterator bumps the index and returns a reference to the next bucket. The iteration is linear over the internal array. Empty buckets are skipped. The complexity is O(N) where N is the number of entries, not the capacity.

The order is random. HashMaps do not preserve insertion order. The order depends on the hash function and the internal layout. If you need deterministic order, you need BTreeMap or IndexMap. Relying on HashMap order is a bug waiting to happen.

The magic that allows for x in &map is the IntoIterator trait. Rust's for loop desugars to a call to into_iter(). The standard library implements IntoIterator for &HashMap, &mut HashMap, and HashMap.

  • &HashMap converts to iter(), yielding (&K, &V).
  • &mut HashMap converts to iter_mut(), yielding (&K, &mut V).
  • HashMap converts to into_iter(), yielding (K, V) by value.

This trait-based dispatch is why the loop syntax adapts to the borrow mode. You don't need to remember three different method names for the loop header. The compiler picks the right one based on whether you pass a reference, a mutable reference, or the owned value.

Functional patterns

Rust iterators support method chaining. You can filter, map, and collect without writing explicit loops. This is useful for transforming data or computing aggregates.

use std::collections::HashMap;

fn main() {
    let mut scores = HashMap::new();
    scores.insert(String::from("Alice"), 100);
    scores.insert(String::from("Bob"), 250);
    scores.insert(String::from("Charlie"), 50);

    // Sum all values. values() returns an iterator over &V.
    let total: u32 = scores.values().sum();
    println!("Total: {total}");

    // Filter winners and collect into a Vec.
    // The filter closure receives (&K, &V).
    let winners: Vec<_> = scores
        .iter()
        .filter(|(_, score)| **score > 80)
        .map(|(name, score)| (name.clone(), *score))
        .collect();

    println!("Winners: {winners:?}");

    // Transform into a new map with doubled scores.
    // Type annotation is needed for the target HashMap.
    let doubled: HashMap<String, u32> = scores
        .iter()
        .map(|(name, score)| (name.clone(), score * 2))
        .collect();
}

The values() method returns an iterator over just the values. This skips the key overhead and is slightly faster if you never touch the keys. The sum() method works on any iterator of numeric types.

When collecting into a HashMap, you often need a type annotation. The compiler can't always infer the key and value types from the iterator alone. HashMap<_, _> tells the compiler to infer the types from the iterator items. If the inference fails, write the full type.

Convention aside: When cloning keys in a map, name.clone() is the standard form. Some developers write String::from(name) or name.to_string(). clone() is preferred because it's explicit about the cost. If the key type changes later, clone() adapts automatically. to_string() forces a string conversion, which might be wrong.

Pitfalls and errors

Iteration errors usually stem from borrowing conflicts or misunderstanding ownership. The compiler catches these at compile time.

If you try to modify the map while iterating, the borrow checker rejects you with E0502 (cannot borrow as mutable because it is also borrowed as immutable). The iterator holds an immutable reference to the map. Inserting or removing requires a mutable reference. You can't have both.

let mut scores = HashMap::new();
scores.insert(String::from("Alice"), 100);

// This fails with E0502.
for (name, score) in scores.iter() {
    // scores.insert(...) would conflict with the borrow in scores.iter().
}

The fix is to collect the changes first, then apply them. Or use retain() if you are removing entries. retain() takes a closure and removes entries where the closure returns false. It handles the borrowing internally.

scores.retain(|_, score| *score > 50);

If you iterate by value, you consume the map. Writing for (k, v) in scores moves the map into the iterator. You get owned keys and values. The map is destroyed after the loop. If you try to use the map afterward, the compiler rejects you with E0382 (use of moved value).

let mut scores = HashMap::new();
scores.insert(String::from("Alice"), 100);

for (name, score) in scores {
    println!("{name}: {score}");
}

// This fails with E0382.
// println!("{scores:?}"); // Error: use of moved value `scores`

Use value iteration only when you want to consume the map. If you need the map later, borrow it.

HashMap iteration order is undefined. If your logic depends on order, you picked the wrong map. Switch to BTreeMap for sorted order or IndexMap for insertion order.

Choosing the right iterator

Pick the iterator that matches your intent. The choice affects performance, safety, and code clarity.

Use iter() when you need to read keys and values without taking ownership. This is the default choice for 90% of loops. It yields (&K, &V) and leaves the map untouched.

Use iter_mut() when you need to modify the values in place. It yields (&K, &mut V). You can update a score or mutate a struct field, but you cannot insert or delete entries.

Use into_iter() when you want to consume the map and take ownership of the keys and values. This yields (K, V). The map is destroyed after the loop. Use this when you are transforming the map into a different structure or you no longer need the map.

Use drain() when you need to empty the map and take ownership of the contents. It yields (K, V) pairs and removes them from the map as you iterate. Use this for batch processing where you want to clear the map atomically. Unlike into_iter(), drain() preserves the map's capacity, so you can reuse the map afterward.

Use keys() or values() when you only care about one side. Calling values() skips the key overhead and is slightly faster if you never touch the keys.

Pick the iterator that matches your intent. If you only need values, don't drag the keys along for the ride.

Where to go next