How to merge two HashMaps

Merge two HashMaps in Rust by calling the extend method on the target map with the source map as an argument.

Merging HashMaps: Extend, Entry, and Ownership

You are building a configuration system. A default configuration lives in a file, loaded into a HashMap. The user provides command-line flags that override specific settings. You need to combine them so the user's choices win, but the defaults fill in the gaps. Or you are aggregating inventory counts from two warehouses and need to sum the values for shared items, not just replace them.

Merging maps sounds simple, but Rust forces you to make explicit choices that other languages hide. Do you want to overwrite collisions or combine values? Do you need the source map to survive the merge, or can it be consumed? The compiler checks these decisions at compile time. Getting them right leads to code that is fast, safe, and impossible to misuse.

The extend method

The standard way to merge one HashMap into another is the extend method. It takes key-value pairs from a source and inserts them into a destination. If a key exists in both maps, the source value overwrites the destination value. The source map is consumed in the process. You cannot use it after the merge.

Think of extend like moving furniture from one house to another. You take the items from the old house and place them in the new house. If the new house already has a sofa, the old sofa gets thrown away and replaced. Once the move is done, the old house is empty. You cannot move the same furniture again.

use std::collections::HashMap;

fn main() {
    // Destination map holds the base configuration.
    let mut base = HashMap::new();
    base.insert("theme", "dark");
    base.insert("font_size", 12);

    // Source map holds user overrides.
    // HashMap::from is the convention for creating maps from literals.
    let user_prefs = HashMap::from([
        ("font_size", 18),
        ("language", "en"),
    ]);

    // extend consumes user_prefs and merges it into base.
    // Colliding keys get overwritten by user_prefs values.
    // The old value for "font_size" is dropped.
    base.extend(user_prefs);

    // base now contains {"theme": "dark", "font_size": 18, "language": "en"}
    // user_prefs is gone and cannot be used.
}

extend is generic over IntoIterator. This means it accepts any type that can be turned into an iterator of key-value pairs. HashMap implements IntoIterator by yielding owned (Key, Value) tuples. This is why extend consumes the source map. It moves the data out of the source and into the destination.

The convention is to use HashMap::from for literal initialization. It reads better than a chain of insert calls. You can also extend with an array of tuples directly, which is useful for adding a few hardcoded entries.

// extend works with arrays too.
base.extend([("debug", false), ("version", "1.0")]);

Trust the consumption. If you need the source map, iterate. If you don't, extend.

When overwrite isn't enough

Real code rarely just overwrites blindly. You often need a custom merge strategy. What if you are merging inventory counts and need to add the numbers instead of replacing them? What if you want to keep the destination value when keys collide? extend cannot do this. It always overwrites.

For custom logic, you iterate over the source map and handle each entry manually. The entry API is the tool for this job. It lets you check for a key and insert or update in a single step, avoiding double hashing.

use std::collections::HashMap;

fn merge_inventory(mut dest: HashMap<String, u32>, src: HashMap<String, u32>) -> HashMap<String, u32> {
    // Iterate over src to process each item.
    // src is consumed here, which is efficient.
    for (item, count) in src {
        // entry API hashes the key once and returns a handle.
        // or_default inserts 0 if the key is missing, since u32 implements Default.
        // This avoids a second hash lookup compared to contains_key + insert.
        *dest.entry(item).or_default() += count;
    }
    dest
}

The entry API pays for itself in performance. Use it when you are touching the map more than once. The pattern *dest.entry(key).or_default() += value is idiomatic for accumulating values. If the value type does not implement Default, use or_insert with an explicit value.

Pitfalls and compiler errors

The most common mistake is trying to use the source map after extend. The compiler rejects this with E0382 (use of moved value). You consumed the map. It is gone.

let mut map1 = HashMap::new();
let map2 = HashMap::new();

map1.extend(map2);

// Error E0382: use of moved value `map2`
// map2.insert("x", 1);

The compiler saves you from using a map that has already been moved. Listen to E0382. It tells you exactly where ownership transferred.

Another trap is trying to merge a map into itself. This fails because extend requires a mutable borrow of the destination, but iterating the source requires an immutable borrow. You cannot borrow the same map as mutable and immutable at the same time. The compiler rejects this with E0502 (cannot borrow as mutable because it is also borrowed as immutable).

let mut map = HashMap::new();
map.insert("a", 1);

// Error E0502: cannot borrow `map` as mutable because it is also borrowed as immutable
// map.extend(map.iter().map(|(k, v)| (k.clone(), v.clone())));

You cannot merge a map into itself. If you need to deduplicate or transform entries in place, use retain or a separate loop that collects results first.

Keeping the source alive

Sometimes you need to merge maps but keep the source map for later use. extend won't work because it consumes the source. You have to iterate over references and clone the data. This introduces a cost. Cloning keys and values allocates memory and copies data. Reserve this approach for small maps or when the cost of cloning is acceptable.

let mut dest = HashMap::new();
let src = HashMap::from([("a", 1), ("b", 2)]);

// Extend with an iterator of references.
// map iterates over &K, &V.
// We must clone keys and values to move them into dest.
dest.extend(src.iter().map(|(k, v)| (k.clone(), v.clone())));

// src is still alive and usable.
println!("src has {} entries", src.len());

Cloning is a tax. Pay it only when necessary. If you can restructure your code to consume the source map, do so. Moving data is faster and safer than cloning.

Performance and capacity

HashMaps grow by doubling their bucket count. If you extend a small map with a large map, the destination might reallocate multiple times. Each reallocation copies all existing entries. This adds overhead.

extend has a specialization for HashMap. When the source is another HashMap, the compiler detects this and reserves the exact capacity needed before starting the merge. This avoids unnecessary reallocations. If you extend with a different iterator type, this optimization does not trigger. You can manually call reserve to hint the capacity.

let mut dest = HashMap::new();
let src: Vec<(String, i32)> = vec![("a".into(), 1), ("b".into(), 2)];

// Reserve capacity to avoid reallocations during extend.
dest.reserve(src.len());
dest.extend(src);

The most efficient merge is the one that moves data instead of cloning it. Consume your maps when you can.

Decision matrix

Use extend when you want to merge one map into another and don't need the source map afterward. It is fast, concise, and moves values efficiently. Use a loop with the entry API when you need a custom merge strategy, like summing values or keeping the destination value on collision. This gives you full control over how collisions are resolved. Use extend with .iter() when you need to merge maps but keep the source map alive. This clones keys and values, so reserve it for small maps or when the cost of cloning is acceptable. Reach for drain_filter or manual iteration when you only want to merge a subset of keys based on a condition. extend merges everything; loops let you filter.

Where to go next