Variables, Mutability & Shadowing
let creates bindings. Immutability is the default; mut opts into reassignment. Shadowing lets you reuse a name with a new type or value without making everything mutable.
Recipe
const MAX_RETRIES: u32 = 3;
fn main() {
let input = "42";
let input: u32 = input.parse().unwrap();
let mut attempts = 0;
attempts += 1;
println!("parsed {input}, attempt {attempts}, max {MAX_RETRIES}");
}When to reach for this: Any time you need clear, local state. Prefer immutable let bindings and shadowing over sprinkling mut everywhere.
Working Example
const DEFAULT_PORT: u16 = 8080;
fn parse_port(raw: &str) -> u16 {
let raw = raw.trim();
let port: u16 = raw.parse().expect("port must be a number");
port
}
fn main() {
let mut log_level = "info";
log_level = "debug";
let port = parse_port(" 3000 ");
println!("port={port}, log={log_level}, default={DEFAULT_PORT}");
}What this demonstrates:
constfor compile-time constants with explicit types- Shadowing
rawafter trimming withoutmut mutonly where reassignment is needed (log_level)- Parsing pipeline that rebinds the same name with a new type
Deep Dive
How It Works
letintroduces a new binding in the current scope- Immutable bindings cannot be reassigned (
x = 1fails) mutallows reassignment of the binding itself- Shadowing creates a new binding that hides the previous one in the same scope
let vs const vs static
| Form | Evaluated | Type required | Use |
|---|---|---|---|
let | Runtime | Inferred or annotated | Local variables |
const | Compile time | Required | Module-level constants |
static | Runtime (fixed address) | Required | Global singletons, FFI |
const KB: u32 = 1024;
static APP_NAME: &str = "myapp";Shadowing in Practice
Shadowing is common in parser chains:
let spaces = " ";
let spaces = spaces.len(); // usize now, not &strUnlike mut, shadowing can change the type because each let is a new binding.
Rust Notes
let (x, y) = (1, 2);
let [first, ..] = [10, 20, 30];Destructuring in let bindings unpacks tuples, arrays, and structs in one step.
Gotchas
- Using
mutwhen shadowing suffices - Parsing pipelines become harder to follow. Fix: Shadow withlet parsed = input.parse()?instead of mutating a string buffer. - Confusing binding mutability with interior mutability -
let x = Rc::new(RefCell::new(0))needs nomutonxto callborrow_mut. Fix: Understand thatmutis about the binding, not heap contents. - Constants in hot paths with runtime values -
constcannot useString::fromat runtime. Fix: Useconstfor literals; usestaticorlazy_static/OnceLockfor runtime init. - Shadowing hides bugs - Reusing names too aggressively obscures which value is live. Fix: Shadow only in short pipelines; use distinct names across long functions.
- Underscore bindings discard intentionally -
let _ = expensive();still runs the expression. Fix: Prefix with_only when the side effect or drop is intended.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
Shadowing (let reuse) | Parse/transform pipelines, type changes | Long functions where name reuse confuses readers |
mut binding | Loop counters, incremental builders | Most local values that are set once |
const | Magic numbers, fixed limits | Values computed at runtime from config |
Cell/RefCell | Shared mutation behind immutable binding | You can use &mut with exclusive access |
FAQs
Why is immutability the default?
It makes data flow easier to reason about and enables the borrow checker to prove safety. Mutation is explicit via mut.
Can I shadow a `mut` binding?
Yes. let mut x = 1; let x = 2; creates a new immutable binding that hides the mutable one.
Does `const` live on the stack?
const values are inlined at compile time. They do not occupy stack space as variables at runtime.
What is the difference between `static` and `const`?
static has a fixed memory address for the program lifetime. const is a compile-time value copied where used.
Can constants be public?
Yes. pub const TIMEOUT_MS: u64 = 5000; is common in library crates.
Is shadowing the same as variable reassignment?
No. Shadowing creates a new binding. Reassignment (mut) updates the same binding.
Can I destructure in `let`?
Yes. let Point { x, y } = point; and let Some(v) = opt else { return }; (let-else) are idiomatic.
Why does `let _x = value` matter for moves?
Even unused bindings can move ownership unless the type is Copy. Use _ prefix if you intentionally ignore a moved value.
Can function parameters be `mut`?
Yes, but it only allows reassigning the local parameter binding. It does not affect the caller.
Should globals be `static mut`?
Avoid static mut. Prefer OnceLock, Mutex, or const data. static mut requires unsafe to read/write.
Related
- Rust Basics - first tour of the language
- Scalar & Compound Types - types behind bindings
- The Type System & Inference - annotations and inference
- Ownership Preview - how moves interact with
let
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.