Shadowing

In Rust, shadowing lets you declare a brand-new variable using the same name as one that already exists, by writing let again. The new binding completely replaces the old one for the rest of the current scope — it can even have a different type — while the original value quietly lives on in memory until it is eventually dropped. This is different from mutating a variable with mut, and understanding that difference is one of the first steps toward reading Rust code fluently.

Overview: What Shadowing Really Does

Every time you write let name = value;, Rust creates a brand-new variable binding in the current scope, even if a variable called name already exists. The old name is not overwritten or destroyed — it simply becomes unreachable by that identifier, because the compiler resolves every later use of name to the newest binding. This is called shadowing: the new variable "shadows" the old one.

Picture it like sticky notes on a shelf. You place a note labeled "x" with the value 5 on the shelf. When you later write let x = x + 1;, Rust does not erase the old note — it reads the old note (5), computes 5 + 1, writes a brand-new note labeled "x" with the value 6, and sets it in front of the old one. From that point on, anyone who asks for "x" finds the new note first. The old note with 5 is still physically on the shelf; it just can no longer be reached by name, and it will be cleaned up (dropped) when its scope ends.

This is fundamentally different from let mut x = 5; x = 6;. With mut, there is exactly one variable, and you are changing the value stored inside it in place — the type can never change, and the compiler enforces that every future assignment matches the original type exactly. With shadowing, nothing is changed in place; you introduce an entirely new, independent variable that merely reuses an old name. Because it is a new variable, it can have a completely different type from the one it shadows, and it never needs mut, since technically no reassignment ever happens.

Aspect mut reassignment Shadowing (let again)
Keyword needed mut on the original declaration none extra — just let again
Number of variables one; its value changes in place a new, independent variable each time
Type must stay exactly the same may change freely
Scope behavior the change is visible everywhere the variable is in scope a shadow declared inside a block disappears when that block ends

Shadowing is also scope-sensitive. If you shadow a variable inside a nested block ({ ... }), an if, a match arm, or a loop body, that shadow only exists until the block ends. Once control leaves the block, the name reverts to whatever it referred to before — the outer binding was never touched. This makes shadowing safe for temporary transformations, but it is also a common source of confusion for readers who expect it to behave like mutation across scope boundaries (see Common Mistakes below).

Shadowing is most useful when you want to carry a single conceptual value through a short pipeline of transformations — for example, taking a raw &str from user input, trimming it, and parsing it into a number — without inventing a new name at every step (input_raw, input_trimmed, input_parsed…). Each let produces a fresh, appropriately-typed variable while keeping one meaningful name throughout.

Syntax

The general form is simply two or more ordinary let statements that happen to reuse the same identifier:

let name = initial_value;
let name = expression_using_name; // creates a NEW binding called "name"
let name = another_expression;    // shadows again, can be a different type
  • let — required every time you shadow; without it, this is a plain assignment, which requires mut and the same type.
  • name — the identifier being reused; each let name = ... introduces a completely separate variable that happens to share this name.
  • expression_using_name — the right-hand side may reference the previous binding of name; it is evaluated using the old value before the new binding takes effect.
  • Type — the new binding’s type is inferred independently each time, so it may differ from the previous binding’s type.
  • Scope — a shadow declared inside { }, an if/match arm, or a loop body only lives until that block ends.

Examples

Example 1: Changing type through shadowing

Here, spaces starts as a &str and is shadowed into a usize holding its length — something mut alone could never do, since mut requires the type to stay fixed.

fn main() {
    let spaces = "   ";
    let spaces = spaces.len();
    println!("Number of spaces: {}", spaces);
}

Output:

Number of spaces: 3

The first spaces is a string slice containing three space characters. The second let spaces = spaces.len(); reads the first binding, calls .len() on it (which does not consume it, since len borrows &self), and creates a brand-new usize binding that shadows the first. From that line on, spaces means the number 3.

Example 2: Shadowing inside a nested scope

This example shows that a shadow declared inside { } only lives for that block; the outer binding is untouched.

fn main() {
    let x = 5;
    println!("Inner scope start, x = {}", x);
    {
        let x = x * 2;
        println!("Inside inner scope, x = {}", x);
    }
    println!("After inner scope, x = {}", x);
}

Output:

Inner scope start, x = 5
Inside inner scope, x = 10
After inner scope, x = 5

Inside the nested block, let x = x * 2; reads the outer x (5), computes 10, and creates a new binding that only exists within that block. Once the block’s closing brace is reached, the inner x is dropped and the name reverts to the original outer binding, which was never modified.

Example 3: A realistic parse-and-transform pipeline

A very common real-world use of shadowing is validating and converting input while reusing one name for the whole pipeline.

fn main() {
    let input = "42";
    let input: i32 = match input.trim().parse() {
        Ok(n) => n,
        Err(_) => {
            println!("Invalid number, defaulting to 0");
            0
        }
    };
    let input = input * 2;
    println!("Doubled value: {}", input);
}

Output:

Doubled value: 84

The first input is a &str. The second let input: i32 = ... shadows it with a parsed integer, using match to handle a possible parse failure instead of an unchecked .unwrap(). The third let input = input * 2; shadows again, doubling the value while staying an i32. Three shadows, one readable name, no throwaway variables like input_str or input_num.

How Shadowing Works Step by Step

Under the hood, the compiler does not overwrite memory when you shadow a variable — each let introduces a distinct entry in the compiler’s scope table and, typically, a distinct storage location. Here is what happens for a shadow like let x = 5; let x = x + 1;:

  1. The first let x = 5; allocates space for an i32 and records that, within the current scope, the name x resolves to this binding.
  2. When the compiler reaches let x = x + 1;, it first evaluates the right-hand side. At this point x still resolves to the original binding, so x + 1 reads the old value (5) and computes 6.
  3. Only after the expression is evaluated does the compiler process the let itself: it creates a new binding for x, stores 6 in it, and updates the scope table so every subsequent use of x in this scope now resolves to the new binding.
  4. The old binding is not destroyed immediately. If its type implements Drop (like String, Vec<T>, or a file handle), the value is dropped when its own scope ends, in reverse order of declaration — it is just no longer reachable by name before that.
  5. When the enclosing scope ends (the closing }), every binding declared in it — shadowed or not — is dropped in reverse declaration order.

This matters in practice: shadowing a String does not free the old string’s heap allocation early. Both the old and new bindings own their own memory until the scope containing them ends.

Common Mistakes

Mistake 1: Reassigning instead of shadowing when the type changes

A very common slip is adding mut and trying to assign a value of a different type into the same variable, instead of using let to shadow it.

fn main() {
    let mut spaces = "   ";
    spaces = spaces.len();
    println!("{}", spaces);
}

Compiler output:

error[E0308]: mismatched types
 --> src/main.rs:3:14
  |
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`

mut only allows changing the value stored in a variable, never its type. spaces was declared as &str, so assigning it a usize is a type error. The fix is to shadow with a fresh let instead, which does not need mut at all:

fn main() {
    let spaces = "   ";
    let spaces = spaces.len();
    println!("{}", spaces);
}

Output:

3

Mistake 2: Expecting a shadow inside a loop to accumulate

This one compiles fine but silently does the wrong thing — a classic runtime footgun. It looks like it should build up a running total, but each iteration shadows a brand-new total that only exists inside that iteration’s loop body.

fn main() {
    let total = 0;
    for i in 1..=5 {
        let total = total + i;
        println!("running total inside loop: {}", total);
    }
    println!("final total: {}", total);
}

Output:

running total inside loop: 1
running total inside loop: 2
running total inside loop: 3
running total inside loop: 4
running total inside loop: 5
final total: 0

Every iteration’s let total = total + i; reads the outer total (which is always 0, since it is never mutated) and creates a fresh inner shadow that is dropped at the end of that iteration’s block. Nothing ever accumulates, and the outer total printed at the end is still 0. To actually accumulate, use a genuinely mutable variable and reassign it, with no shadowing involved:

fn main() {
    let mut total = 0;
    for i in 1..=5 {
        total += i;
        println!("running total: {}", total);
    }
    println!("final total: {}", total);
}

Output:

running total: 1
running total: 3
running total: 6
running total: 10
running total: 15
final total: 15

Best Practices

  • Use shadowing for a short pipeline of transformations on one logical value (raw input → trimmed → parsed → validated) instead of inventing a new name at every step.
  • Never use shadowing to fake accumulation inside a loop — use mut and reassignment (+=, =) for anything that truly needs to build up across iterations.
  • Reach for shadowing specifically when the type needs to change; reach for mut when the type stays the same and the value genuinely mutates in place.
  • Keep shadowing chains short and linear in one scope — three or four steps aids readability, but long chains of the same name can make it hard to know which binding is active at a glance.
  • Remember that a shadow inside { }, if, match, or a loop body is temporary; do not rely on it to change anything the outer scope will see afterward.
  • Prefer match or if let over .unwrap() when shadowing the result of parsing or another fallible operation, so invalid input does not panic your program.

Practice Exercises

  1. Starting from the string slice "3.14", use shadowing to parse it into an f64, then shadow it once more to double the value, and print the final result. Expected output: 6.28.
  2. Without running any code, predict the output of a program that declares let count = 10; at the top of main, then opens a nested block that shadows count as count * 3 and prints it, then prints count again after the block closes. Write down both lines you expect before checking your reasoning against Example 2 above.
  3. Rewrite the flawed loop from Common Mistake 2 so that shadowing is removed entirely and the numbers 1 through 10 are correctly summed using a mutable accumulator. Expected final output: 55.

Summary

  • Shadowing reuses a name for a brand-new, independent variable via let; it is not mutation, and the old value is not overwritten.
  • Unlike mut, shadowing can change a variable’s type, and it never requires the mut keyword itself.
  • Shadowing is scoped — shadows inside { }, if, match, or loops disappear when the block ends, restoring the outer binding.
  • Each use of a name resolves to the nearest preceding binding in that scope, evaluated top to bottom, so the right-hand side of a shadow always sees the old value.
  • Old shadowed values are not dropped early — they live until their own scope ends, exactly like any other binding.
  • Use shadowing for short transformation pipelines on one logical value; use mut for genuine in-place mutation like accumulators and loop counters.