Variables and Mutability
Why Rust variables are immutable by default, and how mut and shadowing give you controlled ways to change them.
2 min read
In most languages, a variable is a variable — you declare it and reassign it whenever you like. Rust flips the default: variables are immutable unless you explicitly say otherwise.
fn main() {
let x = 5;
x = 6; // error: cannot assign twice to immutable variable `x`
}That's not a bug in the example — the compiler genuinely rejects this. It's an intentional design choice.
Why immutable by default?
Immutability by default makes code easier to reason about. When you see let x = 5; and never a mut, you know for a fact that x is 5 everywhere it's used afterward — no need to trace through the function looking for somewhere it might change. In a language where anything can be reassigned anywhere, that guarantee doesn't exist.
It also pays off directly once you get to concurrency: a value that can never change can be shared freely between threads with no risk of one thread seeing a half-updated value.
Opting into mutability
When you do need a variable to change, mark it explicitly with mut:
fn main() {
let mut x = 5;
println!("x is {x}");
x = 6;
println!("x is {x}");
}Now the compiler allows the reassignment. The point isn't that mutation is forbidden — it's that it has to be requested, so it's visible to anyone reading the code (including future you).
Constants
Constants are always immutable, never take mut, and must have their type annotated. They can be declared in any scope, including global scope, and their value must be known at compile time:
const MAX_CONNECTIONS: u32 = 100;Use const for values that are truly fixed — configuration ceilings, mathematical constants — rather than let values you simply don't happen to reassign.
Shadowing
Rust also lets you declare a new variable with the same name as a previous one, called shadowing:
fn main() {
let spaces = " ";
let spaces = spaces.len();
println!("{spaces}");
}This is different from mut: each let creates a genuinely new binding, which can even have a different type — here, spaces goes from a &str to a usize. That would be a type error with mut. Shadowing is common for a short pipeline of transformations on the same conceptual value, without needing a new name at every step:
let input = " 42 ";
let input = input.trim();
let input: i32 = input.parse().unwrap();The takeaway
Default to plain let. Reach for mut only when a value genuinely needs to change over its lifetime, and reach for shadowing when you're transforming a value into a new form rather than mutating it in place. This habit — preferring immutability, adding mutability deliberately — carries through the rest of the language, and it's one of the easiest wins Rust offers for free.
Test what you just learned
4 quick questions. Get all of them right to unlock the next lesson.
You can take the quiz without an account — logging in just lets your result count toward your progress.