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.

fn main() {
let x = 5;
// x = 6; // error[E0384]: cannot assign twice to immutable variable `x`
println!("x = {}", x);
}

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. With shadowing, you create a new variable that happens to have the same name. After the scope ends, the outer binding comes back into view.

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.

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);
}
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?
What does the underscore in 100_000 mean to the Rust compiler?