Variables and Mutability in Rust Explained With Examples
Rust variables are immutable by default. Learn let, let mut, shadowing, constants and the compiler messages you will meet when you change a value you did not mark.

Every language lets you change a variable. Rust makes you ask. That one decision, immutable unless you say mut, shapes how Rust programs read, and it is the first thing the compiler will correct you on. This post shows what the rule is, the two errors you will meet, and the habit that makes the rule feel natural. Every program below was compiled and run on Rust 1.99, edition 2024.
The rule, in a complete program
fn main() {
let name = "Ferris"; // immutable: can never be reassigned
let mut visits = 0; // mutable: may change
visits += 1;
visits += 1;
println!("{name} visited {visits} times");
let total = {
let mut sum = 0;
for n in 1..=4 {
sum += n;
}
sum // mutation stays inside the block
};
println!("total = {total}");
}Ferris visited 2 times
total = 10name is bound once and never changes, so it needs nothing special. visits is declared let mut because the next two lines change it. The block that computes total shows a common shape: the mutation (sum += n) lives inside a small block, and the value that leaves the block, sum on its last line without a semicolon, is immutable again once it is bound to total. You get a mutable accumulator while you need one and an immutable result afterwards.
The reason for the default is practical. When a binding cannot change, a reader who sees let count = 5; on one line knows count is 5 everywhere below it, without scanning the function. The compiler also uses the same fact: it can share the value freely, because nothing will modify it behind its back.
E0384: assigned twice to an immutable variable
Try to change a plain let and the compiler stops you:
fn main() {
let count = 5;
println!("{count}");
count = 6;
println!("{count}");
}error[E0384]: cannot assign twice to immutable variable `count`
--> src/main.rs:4:5
|
2 | let count = 5;
| ----- first assignment to `count`
3 | println!("{count}");
4 | count = 6;
| ^^^^^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut count = 5;
| +++
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0384`.Read the output as three pieces of information. The headline says what rule broke, E0384. The labels show both assignments, the first one at line 2 and the second at line 4. And the help block shows the exact edit: insert mut after let. For a counter or an accumulator that is the right fix.
Before you apply it, ask whether the second assignment really is the same variable. If the new value has a different meaning, a new name is clearer. Writing let total_with_tax = total * 1.2; leaves total available and tells the reader what changed. Reaching for mut by reflex gives up the benefit that the default exists to provide.
Warning: mut you did not need
The opposite mistake is a mut that nothing uses. The compiler notices that too:
fn main() {
let mut greeting = String::from("hello");
println!("{greeting}");
}warning: variable does not need to be mutable
--> src/main.rs:2:9
|
2 | let mut greeting = String::from("hello");
| ----^^^^^^^^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default
warning: 1 warning emittedIt is a warning, not an error, and the program still runs. Treat it as a request to delete the word. A mut that nothing needs is a false claim that something changes the value, and it sends the next reader hunting for a mutation that does not exist. Run cargo clippy -- -D warnings in CI and these never accumulate.
Changing an argument takes &mut
A function that receives a value cannot change it unless the caller agreed. There are two parts to the agreement, and both must be explicit. First attempt: take a shared reference and try to push.
fn add_one(list: &Vec<i32>) {
list.push(1);
}
fn main() {
let mut v = vec![];
add_one(&v);
println!("{v:?}");
}error[E0596]: cannot borrow `*list` as mutable, as it is behind a `&` reference
--> src/main.rs:2:5
|
2 | list.push(1);
| ^^^^ `list` is a `&` reference, so it cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference
|
1 | fn add_one(list: &mut Vec<i32>) {
| +++
warning: variable does not need to be mutableE0596 says the function borrowed *list as mutable while holding only a & reference, which promises read-only access. The help block says what to change: the parameter should be &mut Vec<i32>. The correct program changes the signature and passes &mut v at the call site:
fn add_one(list: &mut Vec<i32>) {
list.push(1);
}
fn main() {
let mut v = vec![];
add_one(&mut v);
add_one(&mut v);
println!("{v:?}");
}[1, 1]The &mut at the call, add_one(&mut v), is not boilerplate. It makes a mutation visible where it happens, so reading add_one(&v) tells you the vector is not changed and add_one(&mut v) tells you it might be. The variable v itself must be declared let mut, because creating a mutable borrow of an immutable binding is not allowed. The rules for how many references can exist at once belong to ownership; the point here is only that mutation is opt-in at both ends.
Shadowing is not mutation
A second let with the same name creates a new variable that hides the first. It looks like reassignment and is not, and the difference has a useful consequence: the new variable can have a different type.
fn main() {
let input = " 42 ";
let input = input.trim(); // still a &str
let input: u32 = input.parse().unwrap(); // now a u32: shadowing may change the type
let input = input * 2;
println!("{input}");
let x = 5;
{
let x = x * 10; // shadows only inside this block
println!("inner x = {x}");
}
println!("outer x = {x}");
}84
inner x = 50
outer x = 5input starts as the string " 42 ", becomes the trimmed string, then the number 42, then 84. Each let input is a distinct binding, and the old one is gone from view. The last lines show scope: inside the braces x is 50, and when the block ends the earlier x is visible again with its value 5. Shadowing in a block does not leak out.
Now try to get the same effect with mut. A mutable variable can change its value but never its type:
fn main() {
let mut input = "42";
input = input.parse::<u32>().unwrap();
println!("{input}");
}error[E0308]: mismatched types
--> src/main.rs:3:13
|
2 | let mut input = "42";
| ---- expected due to this value
3 | input = input.parse::<u32>().unwrap();
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&str`, found `u32`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.E0308 mismatched types. The variable was inferred as &str from "42", and a u32 cannot be stored in it. This is the practical rule for choosing between the two. Use shadowing when each step produces a new kind of value (string, then trimmed string, then number) and mut when the same variable accumulates changes of the same kind (a counter, a buffer, a running total). Shadowing explained goes further into the cases where shadowing is the wrong tool, and E0308 lists the other ways this error arises.
Constants are a different thing
let bindings are computed at run time. A const is a value the compiler substitutes at every use, and it cannot be mut at all:
const MAX_RETRIES: u32 = 3;
fn main() {
let mut attempt = 0;
while attempt < MAX_RETRIES {
attempt += 1;
println!("attempt {attempt} of {MAX_RETRIES}");
}
}attempt 1 of 3
attempt 2 of 3
attempt 3 of 3MAX_RETRIES needs a type annotation (u32), a name in upper case by convention, and a value the compiler can evaluate on its own. Use const for fixed limits, sizes and magic numbers; use let for everything that depends on input or computation. The details of static, const fn and the difference between them are in constants and statics.
Habits that make the rule disappear
- Write
letfirst. Addmutonly when the compiler asks, then read the help text before accepting it. - Prefer building a value in one expression over mutating it in steps.
let total: i32 = (1..=4).sum();has nomut; iterators cover that style. - Keep mutable variables in the smallest scope you can, like the
sumblock in the first example. - When a function needs to change its input, ask whether returning a new value is clearer. Mutable parameters are fine, but a function that returns the changed value is easier to test.
When the rule gets in your way
Immutability by default is a default, not a ban. Algorithms that update state in place, parsers that advance a cursor, loops that fill a buffer, all want mut, and using it there is idiomatic. What is not idiomatic is a function full of let mut variables that each change once. And if you need to mutate through a shared handle, such as a value inside an Rc or behind a &, the type system has RefCell and Mutex for interior mutability, which move the check from compile time to run time; reach for them only after you have confirmed the simpler designs do not fit.


