Skip to content
Select theme

Why would you shadow a variable instead of using mut?

Short answer: shadowing (let x = ... again) creates a brand-new binding that happens to reuse the name — new type, new value, old binding unreachable. mut reuses the same binding, so the type can never change. When you’re converting a value from one type to another, shadowing is the idiomatic move; when you’re changing the value of the same thing over time, mut is.

The classic case: a value starts as a string and ends up as a number. Try it with mut:

cargo build prints:

error[E0308]: mismatched types
--> src/main.rs:3:14
|
2 | let mut spaces = " ";
| ----- expected due to this value
3 | spaces = spaces.len();
| ^^^^^^^^^^^^ expected `&str`, found `usize`
For more information about this error, try `rustc --explain E0308`.

mut lets you assign a new value, and the compiler checks every assignment against the one type the binding was declared with. " " made spaces a &str, so assigning a usize is a type error, no matter how sensible the intent is.

Shadowing is a new binding, so the type can change

Section titled “Shadowing is a new binding, so the type can change”

Write let a second time and it compiles:

spaces = 3

The second let spaces is a completely separate variable that happens to have the same name. It’s a usize from birth — there is no assignment to check against the old &str, because the old binding is simply no longer reachable by this name. Nothing about the first spaces was mutated; it was replaced.

The place this earns its keep: parsing pipelines

Section titled “The place this earns its keep: parsing pipelines”

Converting user input usually takes three steps — trim, parse, use. Without shadowing you either invent a new name per step:

7

or you shadow and keep one name for one concept:

7

Both compile and both print the same thing. The shadowed version reads better once a pipeline grows: the reader knows input is “the thing we’re processing”, and each line says what happened to it. raw/trimmed/number force the reader to keep three names in their head for one value — and six hundred lines later, nobody remembers which of them is current.

There’s a safety side to it too: after let input = input.parse::<f64>(), the old string is unreachable by name. You cannot accidentally use the un-parsed version further down, because there is no name left that refers to it.

Shadowing has a scope — mut doesn’t reset

Section titled “Shadowing has a scope — mut doesn’t reset”

A shadowed binding lives until the end of its block, and the outer one comes back afterwards:

inner: 3
outer: Ada

Inside the block, name is the usize. The block ends, the shadow is dropped, and name is the &str again — the outer binding was never touched. With mut, there is one binding for the whole scope: once you’ve reassigned it, the old value is gone for good.

That makes the choice concrete. Value changes over time, same type the whole way — mut. Converting through types, or replacing “the current version” of something — shadow.