The scoreboard problem
You are building a leaderboard for a local arcade. Players submit scores throughout the day. You need to track the best score for each player name. A player might submit multiple times, and you only care about the highest value. You also need to look up a player's score instantly when they walk up to the screen to check their rank.
In Python, you would reach for a dictionary. In JavaScript, an object or a Map. In Rust, the tool is HashMap. It stores key-value pairs and lets you find values by key in constant time, regardless of how many entries you have. The trade-off is that you must follow Rust's rules for ownership and types. The compiler will help you, but it demands precision.
How HashMap works
HashMap uses a hash function to turn your key into a memory address. Think of it like a post office sorting mail. Instead of reading every address to find a letter, the sorting machine scans the zip code, runs a quick calculation, and drops the letter into the correct bin instantly. You give HashMap a key, it calculates a hash, and jumps straight to the slot where the value lives.
This makes lookups, insertions, and deletions extremely fast. The average time complexity is O(1). You do not scan a list. You compute a location and go there.
The hash function is not magic. It is a trait called Hash that your key type must implement. Rust also requires keys to implement Eq so it can verify equality when two different keys happen to produce the same hash. This collision handling is automatic, but it requires your types to support it.
Minimal example
Here is the basic pattern for creating a map, inserting data, and retrieving a value.
use std::collections::HashMap;
fn main() {
// Create a mutable map. Keys are Strings, values are i32.
// mut is required because insert modifies the map.
let mut scores = HashMap::new();
// Insert entries. Keys are owned Strings.
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Yellow"), 50);
// Look up a value. get returns Option<&V>.
let team = String::from("Blue");
let score = scores.get(&team).copied().unwrap_or(0);
println!("Score for {}: {}", team, score);
}
Notice the mut on scores. You cannot insert into an immutable map. The compiler will reject the code with E0596 (cannot borrow as mutable) if you forget it.
Notice get(&team). You pass a reference to the key. get does not take ownership of the key. It only needs to read it to compute the hash.
Notice .copied(). get returns Option<&i32>. You have a reference to the value inside the map. .copied() converts Option<&T> to Option<T> for types that implement Copy. Since i32 is Copy, this gives you an owned i32 to use outside the map.
Convention aside: If the value type is not Copy, use .cloned() instead. The community prefers .copied() for Copy types because it signals that no allocation happens. .cloned() works for both, but .copied() is more precise.
Reading values safely
get always returns an Option. It never panics if the key is missing. This is a deliberate design choice. Rust forces you to handle the case where the key does not exist. You cannot accidentally dereference a null pointer.
You have several ways to handle the Option. Use unwrap_or to provide a default value. Use unwrap_or_else to compute a default lazily. Use if let to execute code only when the key exists.
let mut inventory = HashMap::new();
inventory.insert("swords", 5);
// Provide a default value when the key is missing.
let shields = inventory.get("shields").copied().unwrap_or(0);
// Compute a default only if needed.
let armor = inventory.get("armor").copied().unwrap_or_else(|| {
println!("Armor not found, checking warehouse...");
0
});
// Execute logic only when the key exists.
if let Some(count) = inventory.get("swords") {
println!("We have {} swords", count);
}
If you try to use the value directly without handling the Option, the compiler rejects you with E0308 (mismatched types). You cannot treat Option<&i32> as i32. The wrapper exists to protect you from missing data.
Trust the Option. If the key isn't there, handle it. The compiler is saving you from runtime crashes.
Keys must be hashable
Not every type can be a key. Keys must implement Hash and Eq. Primitive types like String, &str, i32, and u64 implement these traits automatically. Custom structs do not.
If you try to use a struct as a key without deriving the traits, the compiler rejects you with E0277 (trait bound not satisfied). The error message will tell you exactly which trait is missing.
#[derive(Hash, Eq, PartialEq)]
struct Player {
id: u32,
name: String,
}
fn main() {
let mut stats = HashMap::new();
let p1 = Player { id: 1, name: String::from("Alice") };
// This compiles because Player derives Hash, Eq, and PartialEq.
stats.insert(p1, 100);
}
You must derive Hash, Eq, and PartialEq. Eq implies PartialEq, but the compiler requires both derives explicitly for clarity. Hash generates the bucket index. Eq resolves collisions. Two different keys might hash to the same bucket. HashMap uses Eq to check if the keys are actually identical before returning the value.
Convention aside: When you derive Hash, always derive Eq and PartialEq at the same time. It is a community norm to keep these traits grouped. If a type can be hashed, it must have a well-defined equality.
Realistic example
Here is a pattern you will see often: parsing configuration data. You read lines of text, split them into key-value pairs, and store them in a map.
fn parse_config(lines: &[&str]) -> HashMap<String, String> {
// Pre-allocate capacity if you know the approximate size.
// This avoids reallocations as the map grows.
let mut config = HashMap::with_capacity(lines.len());
for line in lines {
// Split on the first equals sign.
if let Some((key, value)) = line.split_once('=') {
// Trim whitespace from both parts.
let k = key.trim();
let v = value.trim();
// Insert the pair. Overwrites if the key exists.
config.insert(k.to_string(), v.to_string());
}
}
config
}
fn main() {
let raw_lines = [
"host = 127.0.0.1",
"port = 8080",
"debug = true",
];
let config = parse_config(&raw_lines);
println!("Port: {}", config.get("port").unwrap());
}
The with_capacity call is an optimization. HashMap starts small and grows by doubling its size when full. Each growth requires allocating a new table and rehashing all keys. If you know the number of entries ahead of time, hint the capacity. This saves allocations and keeps performance predictable.
Convention aside: Use with_capacity when you have a reasonable estimate. Do not guess wildly. If you overestimate, you waste memory. If you underestimate, you just pay for a few reallocations. A rough count is usually enough.
Common pitfalls
The borrow checker interacts with HashMap in ways that trip up newcomers. The most common error involves holding a reference to a value while trying to modify the map.
let mut map = HashMap::new();
map.insert("key", 42);
// This fails. get holds an immutable borrow of the map.
let val = map.get("key");
// insert requires a mutable borrow.
// You cannot have immutable and mutable borrows at the same time.
map.insert("other", 99); // ERROR E0502
The compiler rejects this with E0502 (cannot borrow as mutable because it is also borrowed as immutable). get returns a reference that lives until the end of the scope. That reference keeps the map immutably borrowed. You cannot insert while the reference is alive.
Fix this by dropping the reference before mutating. Use a block to limit the scope, or copy the value out of the map.
let mut map = HashMap::new();
map.insert("key", 42);
// Copy the value out. The reference ends here.
let val = map.get("key").copied();
// Now the map is free to mutate.
map.insert("other", 99);
Another pitfall is using the wrong type for keys. HashMap requires the key type to match exactly. You cannot look up a String key with a &str unless you use the Borrow trait, which HashMap supports automatically for string types. However, mixing owned and borrowed keys in the same map definition is not allowed. Pick one key type and stick with it.
If you define the map as HashMap<String, V>, you can still call get with &str. The compiler handles the conversion. This is a convenience feature. Do not rely on it for complex types. For custom keys, the types must match or implement Borrow.
Don't fight the borrow checker here. Copy the value or drop the reference before mutating. The map is shared state; Rust makes you prove you aren't corrupting it.
Choosing the right collection
Rust offers several collections for storing data. HashMap is the right tool for many jobs, but not all. Pick the collection that matches your access pattern.
Use HashMap when you need fast lookups by key and order does not matter. It gives O(1) average time for insert, get, and remove. It is the default choice for dictionaries, caches, and frequency counters.
Use Vec when you have a list of items and access by index is enough. Vec is faster than HashMap for sequential iteration and uses less memory overhead. If you only need to store values without keys, reach for Vec.
Use BTreeMap when you need keys sorted or range queries. BTreeMap keeps keys in order and supports operations like "find all keys between A and Z". Lookups are O(log n), which is slower than HashMap but predictable. Use it when order matters or you need to iterate over keys in sorted order.
Use IndexMap when you need insertion order preserved. IndexMap behaves like a HashMap but remembers the order keys were added. It is available in the indexmap crate. Use it when you need fast lookups plus deterministic iteration order.
Use HashMap for speed. Use BTreeMap for order. Use Vec for simplicity. The choice depends on what your code needs to do, not on what feels familiar from other languages.