Shadowing
Learn how variable shadowing in Rust allows redeclaring variables with let - change types, transform values, and keep immutability without mut.
When you need to reuse a variable name in Rust but don't want to make the variable mutable, shadowing is the mechanism you reach for. Unlike mutation, which alters an existing value in place, shadowing declares a brand-new variable that happens to share a name with an earlier one. The new variable hides (shadows) the old one from that point forward in the scope.
Think of variable names as labels stuck to boxes. With shadowing, you stick a new label on a different box and set it in front of the old box. The old box is still there, but you cannot reach it because the new label is the only one you can see. When the new box goes out of scope, the old one becomes visible again.
How Shadowing Works
Shadowing uses the let keyword to introduce a second variable with the same name. The compiler treats this as an entirely new binding. It can have a different type, a different value, and even a different mutability status from the original.
fn main() {
let x = 5;
println!("x before shadowing: {}", x); // 5
let x = x + 1;
println!("x after shadowing: {}", x); // 6
}
The first let x = 5 creates an immutable variable x with the value 5. The second let x = x + 1 does not modify the first x. It creates a new immutable variable, also named x, that takes the value of the expression x + 1 — where x on the right side refers to the previous x. From that line onward, the name x refers to the new variable. The original x still exists in memory until it goes out of scope, but it is unreachable by name.
This works because shadowing is a compile-time concept. The compiler tracks which variable a name points to at each point in the source code. After the second let, every mention of x refers to the new binding.
Shadowing vs. reassignment:
If you attempt to write x = x + 1; without the second let, the compiler will emit error[E0384]: cannot assign twice to immutable variable. Shadowing requires the explicit let keyword because you are creating a new variable, not assigning to an existing one.
Why Shadowing Exists
Shadowing solves a specific set of problems that would otherwise force you to either abandon immutability or create a clutter of differently named variables. Its two main motivations are:
Type transformations while keeping the name clean. A common pattern is parsing a string into a number, then working with the number. Without shadowing, you need separate variable names like input_str and input_num, or you must make the variable mutable and perform a conversion into a new binding anyway. Shadowing lets you reuse the name input after parsing:
fn main() {
let input = "42";
// input is a &str
let input: u32 = input.trim().parse().expect("Not a number!");
// input is now a u32
println!("Parsed number: {}", input);
}
Intermediate transformations on immutable values. When a calculation goes through several steps, shadowing lets you rebind the name at each stage without introducing temporary variables or making anything mutable. This keeps the code tight and the intent clear.
fn main() {
let price = 120.0;
let price = apply_discount(price, 10.0); // first discount
let price = apply_discount(price, 5.0); // additional discount
println!("Final price: {:.2}", price);
}
fn apply_discount(amount: f64, percent: f64) -> f64 {
amount * (1.0 - percent / 100.0)
}
Each let price = ... creates a new binding. The old price is no longer accessible by name, but the logic reads naturally: the same conceptual "price" is refined at each step.
Shadowing in Different Scopes
Shadowing works across scopes in a predictable way. When you shadow a variable inside a nested block, the new variable is visible only within that block. The outer variable remains untouched.
fn main() {
let x = 8;
{
let x = 3; // shadows outer x only inside this block
println!("Inner x: {}", x); // 3
}
println!("Outer x: {}", x); // 8
}
This is fundamentally different from using a mutable variable. If x were mut and you reassigned x = 3 inside the block, the outer x would permanently change. With shadowing, the outer binding is protected.
The same logic applies to function parameters. When you pass a variable to a function, the parameter binding shadows the original name inside the function body without affecting the caller's variable.
fn add_one(x: i32) {
let x = x + 1; // shadows the parameter x
println!("Inside function: {}", x);
}
fn main() {
let x = 5;
add_one(x); // prints 6
println!("Original: {}", x); // 5
}
Mutation inside a block does not automatically revert:
If you declare let mut x = 8; and then inside a nested block you write x = 3; without a let, the outer x is permanently changed to 3. Shadowing with let is the pattern that creates a local, temporary binding that disappears when the block ends.
Shadowing Compared to Mutation
Shadowing and mutation look similar on the surface but serve different purposes and obey different rules. The following short examples show the same output achieved in two ways:
Using mutation with mut:
fn main() {
let mut value = 5;
value = value + 1;
println!("{}", value); // 6
}
Using shadowing:
fn main() {
let value = 5;
let value = value + 1;
println!("{}", value); // 6
}
The key differences emerge when you look at type changes and scope behavior:
- With mutation, you cannot change the type of the variable.
let mut x = 1; x = "hello";is a compile error. Shadowing allows the new binding to have a completely different type. - Shadowing always creates a new variable. Mutation changes the existing one in place. This matters for borrows and references (which will be covered in later chapters), because a shadowed variable is a fresh binding with no outstanding borrows on the old one.
- Inside inner scopes, shadowing isolates changes to that scope. Mutation does not.
Choose mutation when you genuinely need to update a value repeatedly in the same scope and the type stays the same. Choose shadowing when you want to reuse a name for a transformed or converted value without risking side effects on the original.
Do not mix them accidentally:
If you write let mut x = 5; and later write let x = x + 1;, the second let creates a new immutable variable that shadows the mutable one. The original mutable x still exists but is hidden. Any code that tries to mutate the original after that point will not compile because the name x now refers to the immutable shadow. This can be confusing when debugging.
Common Mistakes
Omitting the let keyword when intending to shadow. Writing x = x + 1; on an immutable variable produces a compilation error. Shadowing is not a synonym for reassignment — it requires an explicit let declaration.
Assuming shadowing is the same as mutation. Because the name is reused, it is tempting to think the original variable changed. The original variable still exists and can still be accessed if the shadow goes out of scope. This is especially relevant inside blocks.
Shadowing a variable in a way that breaks readability. While shadowing can keep code clean, excessive reuse of the same name with different meanings in quick succession can make the code harder to follow. Use shadowing when the conceptual identity of the value stays the same but needs refinement; avoid it when the new variable represents something entirely different.
Expecting a shadowed variable to borrow the old one's references. Since shadowing creates a new binding, any borrows on the old variable end at the point of shadowing. This can be useful but surprising if you expected the borrow to carry over.
Shadowing simplifies string-to-number conversion:
A classic correct pattern: let input = "42"; let input: i32 = input.parse().unwrap(); — you now have input as an integer, and the old string binding is inaccessible. The name stays clean, and the code is self-documenting.
A Practical Example: Processing User Input
The following function demonstrates a realistic flow where shadowing naturally keeps the code concise. A raw string input is trimmed, parsed, and then scaled, all under the same name value.
fn process_value(raw: &str) -> Option<f64> {
let value = raw.trim(); // &str
let value: f64 = value.parse().ok()?; // f64, return None if parse fails
let value = value * 1.08; // f64, apply scaling
Some(value)
}
fn main() {
match process_value(" 100.5 ") {
Some(v) => println!("Processed: {:.2}", v),
None => println!("Invalid input"),
}
}
At each step, the name value describes the data in its current form. The code does not need value_str, value_f64, or temporary mutable variables. The transformation pipeline is linear and easy to read.
Summary
Shadowing is the Rust feature that lets you declare a new variable that reuses an existing name, effectively hiding the previous one. It is not mutation — it is a fresh binding that can differ in type, value, and mutability from the original. Use shadowing when you need to change a variable's type, apply a sequence of transformations, or protect the outer value inside a block. It keeps the surface of your code simple while preserving Rust's immutability guarantees.