Skip to content

Variables & Mutability

When you declare a variable with let, it is immutable — you cannot assign to it a second time. This is not a limitation; it is a feature. Immutability by default means the compiler can catch unintended mutations and reason about your code more precisely.

// @expect-error E0384
fn main() {
let x = 5;
x = 6;
println!("x = {}", x);
}

Attempting to reassign an immutable binding is a compile-time error, not a warning:

error[E0384]: cannot assign twice to immutable variable `x`
--> src/main.rs:4:5
|
3 | let x = 5;
| - first assignment to `x`
4 | x = 6;
| ^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
3 | let mut x = 5;
| +++

To allow mutation, add mut after let:

fn main() {
let mut y = 10;
y = 20; // allowed because y is mut
println!("y = {}", y);
}

Shadowing is different from mutation. You re-declare a variable with the same name using a new let binding. The new binding shadows the old one for the rest of the scope. You can even change the type when you shadow.

fn main() {
let z = 3;
let z = z * 2; // shadows the previous z
let z = z + 1; // shadows again
println!("z = {}", z); // 7
// Shadowing can change the type
let word = "hello";
let word = word.len(); // shadow with a usize
println!("len = {}", word);
}

Shadowing is not the same as mut. With mut, you re-assign to the same variable — and that new value must be the same type as the original, because mut only ever changes the value stored in one fixed binding. With shadowing, you create a new variable that happens to have the same name, so its type is free to be completely different, as word (a &str) becoming word (a usize) above shows. After the scope ends, the outer binding comes back into view.

What the Compiler Says: mut Cannot Change Type

Section titled “What the Compiler Says: mut Cannot Change Type”

Because mut reuses the same binding, giving it a value of a different type is a type error, not a shadowing opportunity:

// @expect-error E0308
fn main() {
let mut x: i32 = 5;
x = "a";
println!("x = {}", x);
}
error[E0308]: mismatched types
--> src/main.rs:4:9
|
3 | let mut x: i32 = 5;
| --- expected due to this type
4 | x = "a";
| ^^^ expected `i32`, found `&str`
error: aborting due to 1 previous error
For more information about this error, try `rustc --explain E0308`.

This is the same E0308: mismatched types you will see throughout Rust whenever a value’s type does not match what the context expects — an annotation, a parameter, a return type. mut only lifts the “assigned twice” restriction from E0384; it does not lift the type check.

const declares a compile-time constant. Unlike let, constants:

  • require an explicit type annotation
  • must be set to a constant expression (not a runtime value)
  • are valid for the entire duration of the program in their scope
  • use SCREAMING_SNAKE_CASE by convention
const MAX_POINTS: u32 = 100_000;
const GRAVITY: f64 = 9.81;
fn main() {
println!("MAX_POINTS = {}", MAX_POINTS);
println!("GRAVITY = {}", GRAVITY);
}

The underscore in 100_000 is a numeric separator — it is purely cosmetic and ignored by the compiler. It makes large numbers easier to read.

static also declares a value that lives for the whole program, but it means something different from const: a static has one fixed memory address for the entire run of the program, while a const has no guaranteed address at all — the compiler is free to inline its value at every use site, as if you had pasted the literal in by hand.

static APP_NAME: &str = "rust-deep-dive";
const MAX_RETRIES: u32 = 5;
fn main() {
println!("{} allows {} retries", APP_NAME, MAX_RETRIES);
}
letconststatic
Type annotationoptional (inferred)requiredrequired
Valueany expression, evaluated at runtimeconstant expression, evaluated at compile timeconstant expression, evaluated at compile time
Lifetimeends when its scope endsinlined at every use site — no single addressone fixed memory address for the whole program
Mutable?with mutneverstatic mut exists but requires unsafe to touch — reach for an atomic or a Mutex instead
flowchart TD
  L["let x = 5;"] -->|"scope ends"| D["dropped"]
  C["const MAX: u32 = 100;"] -->|"used"| I["inlined at each call site
(no fixed address)"]
  S["static NAME: &str = ...;"] -->|"used"| A["same address
for the whole program"]
let vs const vs static

In practice: reach for let for almost everything, const for a fixed value you want named (a limit, a default), and static only when you specifically need one shared address — most commonly for FFI or a global you plan to wrap in an atomic type.

fn main() {
let x = 5;
// x = 6; // error: cannot assign twice to immutable variable
let mut y = 10;
y = 20;
println!("y = {}", y);
// Shadowing: re-bind the same name
let z = 3;
let z = z * 2;
let z = z + 1;
println!("z = {}", z);
// Constants are always immutable and require a type annotation
const MAX_POINTS: u32 = 100_000;
println!("MAX_POINTS = {}", MAX_POINTS);
}

Think of let as naming a value, not a memory cell you can freely overwrite. mut upgrades that name to also allow overwriting — but only with a value of the exact same type, because the compiler already fixed the storage size and layout for that binding. Shadowing sidesteps the whole question: it does not reuse storage at all, it introduces a brand-new name that happens to print the same on screen, which is exactly why it is allowed to change type.

Immutable-by-default means most bindings in a Rust codebase carry a compiler-enforced guarantee: nothing downstream can have changed this value. That makes it dramatically easier to reason about a function in isolation — no need to trace every call site to know whether the value read on line 40 could differ from what it was on line 10. The cost is friction: you must add mut deliberately, and forgetting it produces a compile error rather than a runtime surprise, which some newcomers experience as the language “getting in the way” before they internalize that the alternative is a much harder-to-find class of bug.

  • “Shadowing is just a shorthand for mut.” They solve different problems. mut mutates one binding’s value in place (same type, same storage). Shadowing creates an entirely new binding — it can change type, and the old binding is still there (just no longer nameable) until its scope ends.
  • “const and static are interchangeable ways to write a global constant.” const has no guaranteed memory address — the compiler can and does inline it everywhere it is used. static guarantees exactly one address for the whole program. Reach for const unless you specifically need that one shared address.
  • “An immutable variable’s underlying memory can never change.” Immutability is about the binding, not necessarily the bytes — a String behind an immutable let still owns a heap allocation the compiler could theoretically reuse across shadowed rebinds; what let without mut actually guarantees is that you cannot write to it through that name.

AI assistants frequently add mut “just in case” — to a loop counter that is only ever read, or a value that is only reassigned once inside a branch that could just as easily be a fresh let. The compiler will not stop you (an unused mut is a warning, not an error), so this kind of clutter survives code review unless someone is looking for it.

🐛 Spot the bug

fn main() {
let mut total = 0;
for price in [10, 20, 30] {
println!("adding {price}");
}
total = 60;
println!("total = {total}");
}
Show the answer

total is declared mut but the for loop never actually updates it — it is reassigned exactly once afterward, which a plain let mut total = 0; followed by total = 60; does not need at all (and the loop body does not even use total). Compiling this produces warning: variable does not need to be mutable pointing at the let mut total line with help: remove this mut — the real fix here is almost always to actually accumulate inside the loop (total += price;) rather than to silence the warning by deleting mut.

What does the mut keyword do?
What is the key difference between shadowing and mut?
Which of the following is required for a const declaration?
Does this compile?
fn main() {
    let mut x: i32 = 5;
    x = "a";
    println!("x = {}", x);
}
What is the key difference between const and static?
What does the underscore in 100_000 mean to the Rust compiler?