Variables and Mutability

Variable Shadowing

Variable shadowing occurs when a new let statement declares a variable with the same name as a variable that is already in scope. The new binding shadows (hides) the previous binding for all subsequent code in that scope, directing uses of that identifier to the new variable.

Basic Syntax and Behavior

Shadowing always uses the let keyword. Each let statement creates a distinct variable binding with its own storage location:

fn main() {
    let x = 5;
    println!("First binding:  x = {x}");

    let x = x + 1;
    println!("Second binding: x = {x}");

    let x = x * 2;
    println!("Third binding:  x = {x}");
}
First binding:  x = 5
Second binding: x = 6
Third binding:  x = 12

In let x = x + 1;, the right-hand side (x + 1) is evaluated before the new binding x is introduced into scope. As a result, x + 1 reads the value 5 from the first binding, computes 6, and binds a brand-new variable named x to 6.

Step 1: let x = 5;            Step 2: let x = x + 1;        Step 3: let x = x * 2;
Stack                         Stack                         Stack
+---------------+             +---------------+             +---------------+
| x (1st) = 5   | <--- active | x (1st) = 5   | (hidden)    | x (1st) = 5   | (hidden)
+---------------+             +---------------+             +---------------+
                              | x (2nd) = 6   | <--- active | x (2nd) = 6   | (hidden)
                              +---------------+             +---------------+
                                                            | x (3rd) = 12  | <--- active
                                                            +---------------+

All three bindings remain Immutable (let without mut). Once each binding is created, its value cannot be modified through assignment (x = ...), and the compiler may reuse or optimize away earlier stack slots once they are no longer referenced.

Changing Types Across Shadowed Bindings

Because shadowing creates a completely new variable rather than writing into an existing variable's memory slot, the new binding can have a different type from the variable it shadows:

fn main() {
    let spaces = "   ";              // Type: &str
    let spaces = spaces.len();       // Type: usize

    let raw_input = "  42\n";        // Type: &str
    let raw_input = raw_input.trim();// Type: &str
    let raw_input: u32 = raw_input.parse().expect("valid u32"); // Type: u32

    println!("spaces = {spaces}, raw_input = {raw_input}");
}
spaces = 3, raw_input = 42

This avoids inventing single-use variable names such as raw_input_str, trimmed_input_str, and parsed_input_u32 when transforming a value through sequential stages.

Changing Mutability with Shadowing

Shadowing can also change whether a variable name is mutable or immutable at different points in a function:

  • Immutable to mutable: let mut x = x; rebinds an immutable variable or function parameter as mutable.
  • Mutable to immutable ("freezing"): let x = x; rebinds a mutable variable as immutable once initialization or mutation is complete, preventing accidental modification in the rest of the function.
fn main() {
    // 1. Start mutable during construction
    let mut score = 10;
    score += 15;
    score *= 2;

    // 2. Shadow as immutable to freeze the value
    let score = score;
    println!("Frozen score: {score}");

    // 3. Shadow back to mutable if further calculation is needed
    let mut score = score;
    score += 5;
    println!("Updated score: {score}");
}
Frozen score: 50
Updated score: 55

Attempting to mutate score while the immutable shadowed binding is active produces a compile-time error:

// ❌ does not compile
fn main() {
    let mut score = 10;
    score += 5;

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

error: aborting due to 1 previous error

Shadowing Across Inner and Outer Scopes

A variable declared inside an inner block { ... } can shadow a variable from an outer scope. When the inner block ends, the inner binding goes out of scope and the outer variable becomes visible again with its original value and type intact:

fn main() {
    let x = 10;
    println!("Before inner block: x = {x}");

    {
        println!("Start of inner block (still outer x): x = {x}");
        let x = "shadowed inside block";
        println!("After inner let: x = {x}");
    }

    println!("After inner block exits: x = {x}");
}
Before inner block: x = 10
Start of inner block (still outer x): x = 10
After inner let: x = shadowed inside block
After inner block exits: x = 10

Notice that at the start of the inner block—before let x = "shadowed inside block"; executes—the name x still refers to the outer binding (10). Shadowing in Rust is strictly lexical and sequential: a binding only takes effect from the statement where it is declared down to the end of its enclosing block.

Outer Scope {
    let x = 10;               <-- x refers to outer i32 (10)
    Inner Scope {
        println!("{x}");      <-- x still refers to outer i32 (10)
        let x = "inner";      <-- x now refers to inner &str ("inner")
        println!("{x}");      <-- x refers to inner &str ("inner")
    }                         <-- inner x goes out of scope
    println!("{x}");          <-- x refers to outer i32 (10) again
}

Shadowing in Function Parameters, Loops, and Patterns

Shadowing is not limited to standalone let statements. Any construct that introduces a new pattern binding can shadow an existing variable in an outer scope:

Construct Example Scope of Shadowing Binding
Function parameter fn process(x: i32) { let x = x.abs(); } Remainder of the function body
if let / while let if let Some(x) = opt { ... } The if / while body block
match arm pattern match opt { Some(x) => ..., None => ... } That specific match arm
for loop variable for x in 0..3 { ... } The loop body block
fn normalize(value: i32) -> u32 {
    let value = if value < 0 { 0 } else { value as u32 };
    value
}

fn main() {
    let item = Some(99);

    if let Some(item) = item {
        // Inside this block, `item` is `i32` (99), shadowing `Option<i32>`
        println!("Unwrapped item = {item}");
    }

    // Outside the block, `item` is still `Option<i32>`
    println!("Original item = {item:?}, normalized(-5) = {}", normalize(-5));
}
Unwrapped item = 99
Original item = Some(99), normalized(-5) = 0

Lifetimes and Drop Order of Shadowed Variables

Shadowing a variable in the same scope does not immediately destroy or deallocate the shadowed variable. The earlier binding stays alive in memory—merely inaccessible by name—until the enclosing block exits.

When a block ends, variables are dropped in reverse declaration order (last declared is dropped first). Furthermore, a reference taken to the original variable before it was shadowed remains completely valid even after the name is shadowed:

fn main() {
    let x = String::from("original value");
    let reference_to_first = &x;

    // Shadow `x` with a new String
    let x = String::from("second value");

    // `reference_to_first` still points to the first String!
    println!("Current x:          {x}");
    println!("reference_to_first: {reference_to_first}");
}
Current x:          second value
reference_to_first: original value
Stack                                  Heap
+-------------------------+            +------------------+
| x (1st, hidden by name) | ---------> | "original value" |
+-------------------------+            +------------------+
| reference_to_first      | ---------^
+-------------------------+            +------------------+
| x (2nd, active name)    | ---------> | "second value"   |
+-------------------------+            +------------------+
(At block end: 2nd `x` drops first, then `reference_to_first`, then 1st `x`)

Common Errors

Omitting let When Trying to Shadow

Writing x = x + 1; without let attempts to mutate the existing binding x in place rather than shadowing it, which fails if x is immutable:

// ❌ does not compile
fn main() {
    let x = 5;
    x = x + 1;
}
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
3 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
4 |     x = x + 1;
  |     ^^^^^^^^^ cannot assign twice to immutable variable

error: aborting due to 1 previous error

Attempting to Shadow a Constant in the Same Scope

Unlike variables, const items can be matched as constant patterns in let statements. Declaring a const and then writing let with the exact same identifier is treated by the compiler as an attempt to pattern-match against the constant's value rather than shadowing it as a variable:

// ❌ does not compile
const LIMIT: i32 = 100;

fn main() {
    let LIMIT = 200;
}
error[E0005]: refutable pattern in local binding
 --> src/main.rs:5:9
  |
2 | const LIMIT: i32 = 100;
  | ---------------------- constant defined here
...
5 |     let LIMIT = 200;
  |         ^^^^^
  |         |
  |         pattern `i32::MIN..=99_i32` and `101_i32..=i32::MAX` not covered
  |         missing patterns are not covered because `LIMIT` is interpreted as a constant pattern, not a new variable
  |
  = note: `let` bindings require an "irrefutable pattern", like a `struct` or an `enum` with only one variant

error: aborting due to 1 previous error

Following Rust's naming convention—SCREAMING_SNAKE_CASE for const and static items, and snake_case for local variables—prevents this collision entirely.