← thecodex.expert · The Codex Family of Knowledge
Rust

Variables and Mutability

Every variable is immutable unless you explicitly write mut — the opposite default of almost every other mainstream language.

Rust 1.95+ Immutable by default Last verified:
Where this bites you

A let mut counter with a small integer type has a hard ceiling. Increment past it under cargo run and the program panics immediately with "attempt to add with overflow" — which feels like a safety net. Ship the exact same code as cargo build --release, and that safety net is gone: release profiles disable overflow checks by default, so the same increment silently wraps back to the type's minimum instead of panicking, and the program keeps running with a wrong number nobody flagged.

Rustoverflow_bug.rs
fn main() {
    let mut retries: u8 = 255;
    retries += 1;
    println!("retries: {}", retries);
}
// cargo run:            thread 'main' panicked at src/main.rs:3:5: attempt to add with overflow
// cargo run --release:  retries: 0
Canonical Definition

A variable declared with let is immutable by default — attempting to reassign it is a compile error, not a runtime warning. Writing let mut explicitly opts into mutability for that one variable. This default is deliberate: most variables in a well-structured program never need to change, and making mutation opt-in surfaces exactly which parts of the code do change state, which helps both the borrow checker and human readers reason about the program.

let vs. let mut

Reassigning a plain let variable is a compile error, caught before the program ever runs:

Rustmutability.rs
let x = 5;
// x = 6;              // COMPILE ERROR: cannot assign twice to immutable variable

let mut y = 5;
y = 6;                  // fine — y was declared mutable
println!("{}", y);      // 6

That same mut keyword is also where the overflow trap above hides: retries += 1 is a plain mutation, and whether it panics or wraps depends entirely on which profile compiled it. The fix is to stop depending on that profile difference at all — call an explicit, profile-independent method that states what should happen on overflow, such as saturating_add, which caps at the type's maximum instead of panicking or wrapping:

Rustoverflow_fix.rs
fn main() {
    let mut retries: u8 = 255;
    retries = retries.saturating_add(1);
    println!("retries: {}", retries);
}
// cargo run and cargo run --release both print: retries: 255

Constants: always immutable, known at compile time

const declares a value that must be known at compile time and can never be mutable, even with mut — it's a distinct concept from an immutable let variable, which can still hold a value computed at runtime.

Rustconstants.rs
const MAX_RETRIES: u32 = 3;   // must be a compile-time constant, type annotation required

Shadowing: a new variable, not a mutation

Declaring a new variable with let using a name that already exists doesn't mutate the old one — it creates a brand-new variable that happens to reuse the name, optionally with a different type. This is different from mut: the old value still can't be changed, and shadowing can even change the variable's type, which reassignment through mut could never do.

Rustshadowing.rs
let spaces = "   ";        // spaces is a &str
let spaces = spaces.len(); // spaces is now a usize — a NEW variable, different type
println!("{}", spaces);    // 3

Sources

1
The Rust Project. The Rust Programming Language, "Variables and Mutability", doc.rust-lang.org/book/ch03-01.