Variable Shadowing in Rust
Shadowing is one of Rust's more distinctive features — and one that trips up developers coming from other languages. It lets you declare a new variable with the same name as an existing one, replacing the old binding from that point forward. The old value is not changed or destroyed; it is simply overshadowed by the new one.
What Shadowing Looks Like
Re-declare any let binding using the same name and the new declaration takes over.
Each let creates a brand-new variable — the fact that they share a name is just
a convenience for the reader.
fn main() {
let x = 5;
println!("x = {}", x); // 5
let x = x + 1; // shadows the previous x
println!("x = {}", x); // 6
let x = x * 2; // shadows again
println!("x = {}", x); // 12
}x = 5 x = 6 x = 12
let x = ... creates a completely new variable. The originalx = 5 still exists in memory until it goes out of scope — it is just no longer reachable by the name x.Shadowing vs Mutation — The Key Difference
At first glance, shadowing looks similar to assigning a new value with mut. The
critical difference is that shadowing can change the type of the variable, while
mutation cannot.
fn main() {
// --- Shadowing: type can change ---
let value = " 42 "; // &str
let value = value.trim(); // still &str, but trimmed
let value: u32 = value.parse().expect("not a number"); // now u32
println!("parsed: {}", value);
// --- Mutation: type is locked ---
let mut count = 0; // i32
count = count + 1; // still i32, only the value changes
// count = "hello"; // ERROR: mismatched types
println!("count: {}", count);
}parsed: 42 count: 1
Aspect | Shadowing (let x = ...) | Mutation (mut x) |
|---|---|---|
Creates new binding | Yes | No — same binding |
Can change type | Yes | No — type is fixed |
Original value | Still exists until scope ends | Overwritten in-place |
Requires mut | No | Yes |
Works on immutable bindings | Yes | No |
Inner scope shadow disappears | Yes | No — change persists |
The Classic Use Case — Parsing Input
The most common reason to shadow is transforming a value through multiple steps where
each step produces a conceptually "same thing, just more refined" result. The canonical
Rust example is reading user input as a String and then parsing it into a number:
use std::io;
fn main() {
let mut input = String::new();
println!("Enter a number:");
io::stdin().read_line(&mut input).expect("Failed to read line");
// Shadow: same name, but now it's a u32 instead of a String.
// This is idiomatic Rust — no need for a different name like input_number.
let input: u32 = input.trim().parse().expect("Please enter a valid number");
println!("You entered: {}", input);
println!("Doubled: {}", input * 2);
}input_number or parsed, but that adds noise. Shadowing says "this is the same logical thing, just at a further stage of processing." The reader does not need to track two names for one concept.Shadowing in Inner Scopes
A shadow created inside an inner block (a {} scope) is local to that block.
When the block ends, the inner shadow disappears and the outer variable is visible again.
This is different from mutation, where a change made inside a block persists after it.
fn main() {
let x = 10;
println!("outer x = {}", x); // 10
{
let x = x * 5; // inner shadow — only lives in this block
println!("inner x = {}", x); // 50
}
println!("outer x = {}", x); // 10 — shadow is gone
// Compare with mut: the change persists outside the block
let mut y = 10;
{
y = y * 5; // mutation of the outer y
}
println!("y after block = {}", y); // 50 — change survived
}outer x = 10 inner x = 50 outer x = 10 y after block = 50
Shadowing to Change Type Without a New Name
Shadowing really shines when processing data through a pipeline of transformations. Each step can refine the type while keeping a single, meaningful name:
fn main() {
// Raw CSV line from a file
let record = " Alice,28,engineer ";
// Step 1: trim whitespace
let record = record.trim();
// Step 2: split into fields
let record: Vec<&str> = record.split(',').collect();
// Step 3: extract structured values
let name = record[0];
let age: u8 = record[1].parse().expect("bad age");
let role = record[2];
println!("Name: {}", name);
println!("Age: {}", age);
println!("Role: {}", role);
}Name: Alice Age: 28 Role: engineer
Shadowing in Loops
Shadowing works naturally inside loops. Each iteration can shadow a variable to refine it without needing a mutable binding or a chain of distinct names.
fn main() {
let words = vec![" hello ", " world ", " rust "];
for word in &words {
let word = word.trim(); // shadow: &str trimmed
let word = word.to_uppercase(); // shadow: String, uppercase
println!("{}", word);
}
}HELLO WORLD RUST
When Shadowing Improves Clarity
Shadowing is a net positive in several clear situations:
Type transformation pipelines — converting the same logical value through multiple representations (String → &str → u32)
Parsing user input — the classic
let guess: u32 = guess.trim().parse()...idiomNarrowing an optional — shadowing after an
if letormatchgives the unwrapped value the same nameRemoving noise from names — avoids
raw_value,trimmed_value,parsed_valueclutter for what is conceptually one thingImmutable refinement — each shadow is immutable by default, making the final value clearly fixed
When Shadowing Adds Confusion
Shadowing is not always the right tool. Avoid it when:
The types are unrelated — shadowing an
i32with aVec<String>of the same name obscures intentThe shadowing is far from the original — if 20 lines separate the two declarations, readers may miss the shadow
Inside deeply nested blocks — stacking shadows across many scopes makes the code hard to trace
When a better name exists — if a distinct name genuinely adds clarity, use it
mut; when you want a new refined binding, use shadowing.Shadowing with if let and match
A natural pattern in Rust is to shadow an Option or Result variable after
unwrapping it, so the unwrapped value carries the same meaningful name:
fn find_user(id: u32) -> Option<String> {
if id == 1 { Some(String::from("Alice")) } else { None }
}
fn main() {
let user = find_user(1); // Option<String>
// Shadow user inside the if block — now it's a String, not Option<String>
if let Some(user) = user {
println!("Found user: {}", user);
println!("Name length: {}", user.len());
} else {
println!("User not found");
}
}Found user: Alice Name length: 5
if let Some(x) = x shadow is idiomatic and encouraged. The alternative — naming the unwrapped value user_name orinner_user — is more verbose without adding clarity.Full Example — Shadowing Through a Pipeline
Processing user registration input
fn is_valid_username(s: &str) -> bool {
s.len() >= 3 && s.chars().all(|c| c.is_alphanumeric() || c == '_')
}
fn register(raw_username: &str, raw_age: &str) {
// Step 1: trim whitespace from both inputs
let raw_username = raw_username.trim();
let raw_age = raw_age.trim();
// Step 2: parse age from &str to u8
let raw_age: u8 = match raw_age.parse() {
Ok(n) => n,
Err(_) => {
println!("Invalid age: {}", raw_age);
return;
}
};
// Step 3: validate username (shadowing to lowercase version)
let raw_username = raw_username.to_lowercase();
if !is_valid_username(&raw_username) {
println!("Invalid username: {}", raw_username);
return;
}
println!("Registered: {} (age {})", raw_username, raw_age);
}
fn main() {
register(" Alice_99 ", " 25 ");
register(" BOB ", " 30 ");
register(" x! ", " 22 ");
register(" carol ", " abc ");
}Registered: alice_99 (age 25) Registered: bob (age 30) Invalid username: x! Invalid age: abc