Coding by Hand
Rust home

Variables and Shadowing

A variable in Rust is a locker at the gym. You walk up to an empty cubby, slide your gear inside, and clip a paper name card to the door. The card is the variable name. The cubby holds the value. From that point on, anyone who says the name knows which cubby to look in. The trick Rust pulls — the one that surprises people coming from Python or JavaScript — is that the door locks behind you. Once you put your gear in and clipped the card, the cubby will not accept new gear unless you told it ahead of time that you wanted a lock that opens both ways.

A let binding is a gym locker with a paper name card clipped to its door.
A let binding is a gym locker with a paper name card clipped to its door.

The locked-by-default rule did not start with Rust. It came out of a quiet war fought through the 1980s and 90s between two camps of programmers. One side, led by people like Bjarne Stroustrup at Bell Labs, built C++ on the idea that every variable should be mutable unless you wrote const in front of it — the door is unlocked unless you bolt it. The other side, working on languages like Standard ML and later Haskell, said the opposite — the door should be locked unless you bolt it open. Their argument was simple. Most variables in most programs never need to change after they are set. If the default is "anyone can change this," then every reader of the code has to assume the worst and trace every line to be sure. If the default is "no one can change this," the reader relaxes. When Graydon Hoare started Rust at Mozilla in 2010, he picked the second camp. The keyword to claim a locker is let, and let alone gives you the locked door.

fn bind() {
    let reps = 8;
    println!("locker reps holds: {reps}");
    // reps = 10; // would not compile: cannot assign twice to immutable binding
}

Read it like a label-maker — the word reps is the card you taped on the door, the number 8 is the gear inside. Try to add a line that says reps = 10; after the binding and the compiler stops you cold with "cannot assign twice to immutable binding." The cubby is locked. If you want a cubby with a two-way lock, you ask for it on the day you claim it by writing mut.

fn mutate() {
    let mut weight = 135;
    println!("locker weight starts at: {weight}");
    weight = 145;
    println!("locker weight now holds: {weight}");
}

Now the same cubby holds 135, then holds 145, and the card on the door never changed. This is real assignment — same locker, same address in memory, fresh value sitting in the slot. The compiler watches the path the data takes and proves to itself that nothing else is reading the old value at the moment you swap it. That single rule — mutable only when you say so, and only one writer at a time — is how Rust avoids a whole class of bugs that haunt every C and C++ codebase ever shipped. Microsoft published a number in 2019 that landed hard on the industry: seventy percent of the security bugs they patched every year came from memory written when something else thought it was safe to read. Rust's let vs let mut is the first brick in the wall that keeps that from happening.

Two adjacent lockers: one with a one-way bolt, one with a two-way swivel lock.
Two adjacent lockers: one with a one-way bolt, one with a two-way swivel lock.

Here is the part that trips up everyone who has used another language. Rust lets you write let reps = 8; and then, three lines later, write let reps = "eight"; — and it compiles. The second let is not changing the cubby. It is claiming a brand new cubby down the row and stapling the same name card on top of the old one. The old cubby is still there, still holding the number 8, but the name reps now points at the new cubby holding the word "eight". This is called shadowing, because the new binding casts a shadow over the old one. Anyone who says reps after that line gets the shadow, not the original.

The reason this exists is practical. Read in user input as a string, then parse it into a number — same idea, different type. In other languages you would invent a second name like reps_str and reps_int. In Rust you reuse the name and let the second let change the type underneath it.

fn shadow() {
    let plate = "45";
    println!("plate is text: {plate}");
    let plate: u32 = plate.parse().expect("digits only");
    println!("plate is now a number: {plate}");
    let plate = plate * 2;
    println!("plate doubled: {plate}");
}

Three cubbies in a row, each with a fresh paper card that says plate. The first one holds the text "45". The second one — claimed by let plate: u32 = plate.parse()... — holds the number 45, and the act of parsing reads the old cubby's text before the new card covers it. The third holds 90, doubled from the second. The compiler tracks each cubby separately because each let is a brand new binding. Run the whole binary and you can watch every step.

locker reps holds: 8

locker weight starts at: 135
locker weight now holds: 145

plate is text: 45
plate is now a number: 45
plate doubled: 90

The first two blocks print the locker rules in plain numbers. The shadowing block then walks the same name through three different cubbies. Notice that the third cubby's let plate = plate * 2; had to read the second cubby first — plate on the right side of the = still meant the number 45 at that moment, because the new card only goes up after the right side is computed. That ordering is what makes shadowing safe instead of confusing. The old binding lives until the new one is fully assembled.

Shadowing stacks new name cards on fresh lockers while the old ones remain underneath.
Shadowing stacks new name cards on fresh lockers while the old ones remain underneath.

One question worth asking — why bother having shadowing at all when let mut already lets you change a value? Because mut cannot change the type. A let mut x: u32 is locked to u32 for the rest of its life; you can swap the number inside but you cannot make it a string. Shadowing gives you a clean way to evolve the type as the data moves through stages — bytes become a string, the string becomes a number, the number becomes a struct — without ever leaving a stale half-converted value sitting around with the same name.

Next lesson — the numbers inside those lockers are not infinite. A u32 can hold values up to about four billion and then something has to give, and what gives depends on whether you built the program for debug or release.