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 requiresmutand the same type.name— the identifier being reused; eachlet name = ...introduces a completely separate variable that happens to share this name.expression_using_name— the right-hand side may reference the previous binding ofname; 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
{ }, anif/matcharm, 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;:
- The first
let x = 5;allocates space for ani32and records that, within the current scope, the namexresolves to this binding. - When the compiler reaches
let x = x + 1;, it first evaluates the right-hand side. At this pointxstill resolves to the original binding, sox + 1reads the old value (5) and computes 6. - Only after the expression is evaluated does the compiler process the
letitself: it creates a new binding forx, stores 6 in it, and updates the scope table so every subsequent use ofxin this scope now resolves to the new binding. - The old binding is not destroyed immediately. If its type implements
Drop(likeString,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. - 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
mutand reassignment (+=,=) for anything that truly needs to build up across iterations. - Reach for shadowing specifically when the type needs to change; reach for
mutwhen 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
matchorif letover.unwrap()when shadowing the result of parsing or another fallible operation, so invalid input does not panic your program.
Practice Exercises
- Starting from the string slice
"3.14", use shadowing to parse it into anf64, then shadow it once more to double the value, and print the final result. Expected output:6.28. - Without running any code, predict the output of a program that declares
let count = 10;at the top ofmain, then opens a nested block that shadowscountascount * 3and prints it, then printscountagain after the block closes. Write down both lines you expect before checking your reasoning against Example 2 above. - 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 themutkeyword 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
mutfor genuine in-place mutation like accumulators and loop counters.
