← Fundamentals
Guided lesson 7 questions · Rust only

Rust let, mut, and shadowing

let keeps a value, let mut lets it change, and a second let makes a new binding. Follow each one.

A Rust binding can be assigned again, or used to change its value, only when it is declared with mut. A second let with the same name changes nothing: it makes a new binding, which may have a new type, and the earlier binding is hidden, not changed.

Written for Rust.

Before you begin

Why this matters

Rust code often uses the same name for several bindings, and errors about mut are among the first a new Rust programmer meets. If you can say which binding a name means on each line, you can read those errors, choose between mut and a second let, and spot a line that uses the wrong binding.

What this helps you reason about

  • Predict which lines the compiler rejects for a binding without mut, and which fix it suggests.
  • Follow a value that changes form, such as typed text that becomes a number, under one name.
  • Spot a line that reads a binding you did not mean: an older one, a newer one, or one made by a match arm.

Many languages have one keyword for a name whose value can change and one for a name whose value cannot, such as let and const in JavaScript. Rust has three forms: let, let mut, and a second let with a name that already exists. Each one does something different. This lesson follows each binding, one let at a time, and shows what the compiler allows for each.

Already know this? The short version
  • A binding without mut can be neither assigned again (E0384) nor changed in place or lent as &mut (E0596); let mut allows all three, and rustc warns when a mut is never needed. The stepper does not show that warning.
  • mut belongs to the binding, not the value: let order = order; makes a Vec fixed, and let mut order = order; makes it changeable again.
  • A let with an existing name is shadowing: a new binding, which may have a new type, such as text parsed into a u32. The earlier value is hidden, not changed, and lives until its own block ends; a shadow inside a block ends at that block’s closing brace.
  • A const item needs a written type and a value the compiler can compute (E0435). The section “What goes wrong with let and mut in real programs?” covers the old meaning used after a shadow, let mut everywhere, a loop variable with an outer name, and a plain name in a match arm.

Go to the final check· Go to the practice tickets

Words in this lesson (8)
binding
A name attached to a value, created with let. Without mut, the binding cannot be assigned again.
mut
Short for mutable. let mut makes a binding that can be assigned again and used to change its value.
mutation
Changing a value in place, such as songs.push("finale") or songs[1] = "finale". It needs a binding declared with mut, or a mutable reference.
mutable reference
A reference written &mut x, of type &mut T, which can change x. While it is in use, x cannot be used in any other way, not even by its owner.
compiler warning
A message from the compiler about code that compiles but looks like a mistake, such as a mut that nothing needs. The program is still built and can run.
shadowing
Declaring a new binding with let that has the same name as an earlier one. From that line on, the name means the new binding; the earlier binding is not changed.
parse
To read text and build a value from it, such as the number 21 from the text "21". Parsing fails when the text does not have the expected form.
const item
A value declared with const outside any function, such as const MAX_SONGS: usize = 12;. The compiler copies its value into each place it is used, so it can never change.

Examples are focused snippets; imports, enclosing functions, or classes may be omitted. C examples use C11; Haskell examples use Haskell 2010. Expand the notes for language-specific depth.

01 / let

What does let without mut promise?

Mutability and constants showed that a Rust binding without mut protects its value as well as its name. This lesson follows each binding closely, one let at a time.

A let statement makes a binding: a name attached to a value. Without mut, the binding keeps that value until the end of its block. An assignment to it is a compile error, and the compiler reports: error[E0384]: cannot assign twice to immutable variable `crates`.

The compiler points at the assignment and suggests mut crates. That is one fix. The other is a second let, which is allowed: let crates = crates - 5; makes a new binding with the same name. Its value is computed from the first crates, and from the next line on, crates means the new binding.

main.rs
let crates = 12;
// crates = crates - 5; // error[E0384]
let crates = crates - 5; // a new binding
println!("{} crates left", crates); // 7 crates left

A binding without mut keeps its value; a second let makes a new binding instead of changing the first one.

Coming from JavaScript: Rust’s let is closer to JavaScript’s const, and stricter: a Vec in a binding without mut cannot change either. Rust’s let mut is closer to JavaScript’s let.

Precisely: a binding given its value later

let crates; with no value is allowed, and so is one later assignment, crates = 12;, without mut. The compiler checks that crates gets a value on every path before it is read, and that it never gets a second one; a second assignment is E0384 again. Before that assignment, the stepper can show a leftover value for crates, such as 0.

Experiment

Follow two bindings named crates

Line 3 is a let statement, so it makes a second binding named crates. Its value is computed from the first crates: 12 - 5. Without let, line 3 is an assignment to the first crates, which was declared without mut.

Why it matters A let with a name that already exists makes a new binding; it never assigns. An assignment needs a binding declared with mut.

Predict what is printed. Then remove let from line 3, so that it reads crates = crates - 5;, and read the compiler error.

fn main() {
    let crates = 12;
    let crates = crates - 5;
    println!("{} crates left", crates);
}
What to look for

At the stop on line 3, crates shows 12. At the stop on line 4, crates shows 7, and the run prints 7 crates left. The panel lists one crates at each stop: it shows the newest binding with that name. The first crates is hidden there, not changed. After the edit, the compiler reports error[E0384]: cannot assign twice to immutable variable `crates`, pointing at line 3, with help: consider making this binding mutable: `mut crates` under line 2.

Expected output

7 crates left

02 / mut

What does mut allow?

let mut gives a binding two permissions. It can be assigned again, and its value can be changed in place, which is called mutation. push adds an element at the end of a Vec, Rust’s growable list, so it changes the Vec in place. So can code that holds a mutable reference, written &mut order: a reference that can change the value it points to. Taking one from a binding without mut, or calling push on it, gives error[E0596]: cannot borrow `order` as mutable, as it is not declared as mutable.

mut belongs to the binding, not to the value. let order = order; moves the Vec into a new binding without mut: the same Vec, now fixed. Mutability and constants showed the opposite move, into a binding with mut. So to know whether a value can change, look at the binding that holds it now.

A mut that nothing needs is not an error. rustc builds the program and prints a compiler warning, a message about code that compiles but looks like a mistake: warning: variable does not need to be mutable, with help: remove this `mut`. The stepper does not show Rust warnings. Run cargo check or cargo build in a terminal to see them.

main.rs
let mut order = Vec::new();
order.push("cod"); // mutation: needs mut
let more = &mut order; // needs mut too
more.push("hake");
let order = order; // the same Vec, now fixed
println!("{:?}", order); // ["cod", "hake"]

mut lets a binding be assigned again and lend its value for changes. It belongs to the binding, so moving the value into a binding without mut fixes it.

Coming from C: A C object declared const int never changes. A Rust value held without mut can still change after it moves into a binding with mut.

Precisely: a mutable reference without mut

let more = &mut order; needs order to be mut, but more itself needs no mut: more is never assigned, and push changes the Vec that more points to. While more is in use, order cannot be used in any other way. Rust borrowing and references covers these rules.

Experiment

Change an order through mut, then stop the changes

push and &mut order both need permission to change the Vec, and that permission comes from the binding that holds it. let order = order; moves the Vec into a new binding without mut. The Vec is the same one; the new binding gives no permission to change it.

Why it matters To stop changes once a value is built, move it into a binding without mut. Whether a value can change depends on the binding that holds it now.

Predict what is printed. Then add the line order.push("plaice"); between lines 6 and 7, and read the compiler error.

fn main() {
    let mut order = Vec::new();
    order.push("cod");
    let more = &mut order;
    more.push("hake");
    let order = order;
    println!("{:?}", order);
}
What to look for

At the stop on line 3, order shows [] (len=0, cap=0, data=0x8): an empty Vec has no memory for elements yet, and 0x8 is a placeholder, not a real address. At the stop on line 5, more shows an address, then → ["cod"], with the same data= address as order: one Vec, reached through two names. At the stop on line 7, the panel lists the new order first, holding "cod" and "hake" at the same data= address, so the elements did not move. more is still listed, although its last use was on line 5. The run prints ["cod", "hake"]. After the edit, the compiler reports error[E0596]: cannot borrow `order` as mutable, as it is not declared as mutable, pointing at order.push("plaice"), with help: consider changing this to be mutable: `mut order` under line 6.

Expected output

["cod", "hake"]

03 / Shadowing

How is a second let different from a change?

A second let with the same name is shadowing. It looks like a change, but nothing is changed: the earlier binding is hidden, and its value stays where it is.

Because the binding is new, it may have a new type. Text from a scale or a form arrives as text, and code often keeps the name while the value changes form. String::from(" 12 ") makes a String that holds the text. trim gives a &str: a reference to part of that text, without the spaces at each end. parse reads the text as a number of the type written after the name, here u32. It can fail, so unwrap_or(0) gives the number, or 0 when the text is not a whole number.

The earlier value still exists until the end of its block. Below, the trimmed &str points into the first crates, the String. An assignment could not do any of this: it puts a new value of the same type into the same binding.

main.rs
let crates = String::from(" 12 ");
let crates = crates.trim(); // a &str into the String
let crates: u32 = crates.parse().unwrap_or(0);
let crates = crates + 3;
println!("{}", crates); // 15

Shadowing makes a new binding, which may have a new type; the earlier value is hidden, not changed.

Coming from Python: In Python, count = int(count) gives one name a value of another type. In Rust, let count: u32 = count.parse().unwrap_or(0); does it with a second binding, and the compiler knows the type of each one.

Precisely: when a shadowed value is dropped

A shadowed value is dropped at the end of its block, like any other value, not at the line that hides it. When the new binding takes the value, as in let order = order;, the value moves, and the old binding has nothing left to drop. Rust ownership and moves explains moves and drops in full.

Experiment

Turn typed text into a number under one name

Each let crates makes a new binding, and each new binding has its own type: String, then &str, then u32 twice. The &str is a reference into the String, so the String must stay alive; it does, until the end of main, because a shadowed value is hidden, not dropped.

Why it matters Shadowing can change a value’s type while keeping its name. The compiler then rejects any use that needs the old type.

Predict what is printed. Then add the line println!("{}", crates.len()); just above line 5, and read the compiler error.

fn main() {
    let crates = String::from(" 12 ");
    let crates = crates.trim();
    let crates: u32 = crates.parse().unwrap_or(0);
    let crates = crates + 3;
    println!("{}", crates);
}
What to look for

At the stop on line 3, crates shows " 12 " (len=4, cap=4, data=0x…): a String. At the stop on line 4, it shows "12" (len=2, data=0x…) with no cap: a &str, and its data= address is one more than the String’s. So the trimmed text is part of the first binding’s String, which still exists although the panel no longer lists it. At the stop on line 5, crates shows 12, a number, and at line 6 it shows 15. The run prints 15. After the edit, the compiler reports error[E0599]: no method named `len` found for type `u32` in the current scope: from line 4 on, crates means the number.

Expected output

15

04 / Blocks

Where does a shadow end?

A binding lives until the closing brace of its block, as Scope and shadowing showed, and a shadow is no exception. A let inside an inner block hides the outer binding until that block ends. After the brace, the name means the outer binding again, with its old value.

To bring a result out of a block, make the block give it. A block’s value is its last expression, written without a semicolon, so let kilos = { … }; keeps the steps inside the block and only the result outside. Below, the inner grams is the weight without the 300 g of ice. It ends at the brace, and the outer grams still holds the weight with the ice.

main.rs
let grams = 2300;
let kilos = {
    let grams = grams - 300; // without the ice
    grams / 1000
};
println!("{} {}", grams, kilos); // 2300 2

A shadow inside a block ends at its closing brace. To keep a result, make it the block’s value.

05 / const

When is a const item better than let?

A const item, as Mutability and constants showed, has a written type and a value that the compiler copies into each place it is used. Three things separate it from a let binding.

The compiler must be able to compute the value. A const built from a let binding is refused: error[E0435]: attempt to use a non-constant value in a constant. A let binding can hold any value, including one computed while the program runs, such as a parsed weight.

A const item can be written outside every function, so every function in the file can use it. A let binding exists only inside its block.

By convention, a const item’s name is written in capitals, such as ICE_GRAMS. In a match, described in the next section, that difference matters. The stepper does not list const items in the panel; it lists let bindings and parameters.

main.rs
const ICE_GRAMS: u32 = 300;

fn main() {
    let grams = 2300; // could come from a scale
    println!("{}", grams - ICE_GRAMS); // 2000
}

Use a const item for a fixed value known before the program runs, and let for a value the program computes.

Coming from Java: A Java static final field set in a static block can be computed when the program starts; a Rust const item must be computed by the compiler.

Precisely: a let with a const item’s name

let ICE_GRAMS = 250; inside main does not shadow the const item. The compiler reads ICE_GRAMS there as a pattern that compares with 300, and reports error[E0005]: refutable pattern in local binding, with the note interpreted as a constant pattern, not a new variable. Lower-case names for bindings and capitals for const items avoid this.

06 / In production

What goes wrong with let and mut in real programs?

Four mistakes with let and mut are common in real code.

The old meaning used after a shadow. let price = price / 100; turns cents into euros under the same name and the same type. A line further down that still expects cents compiles, and it is wrong by a factor of 100. When the meaning changes but the type does not, use a new name, such as price_euros. A shadow that changes the type is safer: the compiler rejects a use of the old meaning.

let mut everywhere. When every binding is mut, the code does not say which values change, and a value a later line needs may already have changed. Use let by default, give each step its own binding, and keep mut for values that really change, such as a running total. The unused mut warning finds a mut that is never used, not one used once too often.

A loop variable with the name of an outer binding. for price in prices makes a new price for each pass of the loop body. After the loop, price means the outer binding again, not the last price.

A name in a match arm. match compares a value with each pattern from the top and gives the value of the first arm that fits; _ fits any value. A plain name in a pattern does not compare: it fits every value and makes a new binding. To compare with a fixed value, write a literal, or a const item with its capital name.

main.rs
let special = "mackerel";
let price = match fish {
    special => 300, // fits every fish
    _ => 500,
};

Give a new meaning a new name, keep mut for values that change, and never use a lowercase name to compare in a match.

Experiment

Match a fish against a name

A plain name in a pattern is not compared with anything. It fits every value and makes a new binding, here a second special that holds "cod". A text literal is compared, so "mackerel" fits only mackerel.

Why it matters To compare with a value in a match, write a literal or a const item, never a lowercase name.

special holds the special of the day, mackerel. Predict the price printed for cod. Then change special => 300, to "mackerel" => 300, and predict again.

fn main() {
    let special = "mackerel";
    let fish = "cod";
    let price = match fish {
        special => 300,
        _ => 500,
    };
    println!("{} {}", fish, price);
}
What to look for

At the stop on line 5, the arm special => 300 has been chosen, and the next stop is line 8. The panel still shows special as "mackerel": it lists the special from line 2, not the new special that the arm made, which holds "cod". The run prints cod 300. After the edit, the stepper stops on line 5, then on line 6, the _ arm, and the run prints cod 500. rustc reports warning: unreachable pattern under _ in the original program, but the stepper does not show Rust warnings.

Expected output

cod 300

07 / Try it

Can you follow every binding?

Follow each binding, and decide for each line whether it makes a new binding or changes one.

main.rs
fn main() {
    let weight = "800";
    let weight: u32 = weight.parse().unwrap_or(0);
    let mut total = weight;
    {
        let total = total + 200;
        println!("{}", total);
    }
    total = total * 2;
    let total = total;
    println!("{} {}", weight, total);
}

What are the two output lines?

Reveal the reasoning

1000, then 800 1600. weight starts as text, and a second weight holds the number 800. total is a mut binding that starts at 800. Inside the block, let total makes a new binding, 1000, which is printed and ends at the brace. total = total * 2 assigns the outer total, which still holds 800, so it becomes 1600. let total = total; then moves the value into a binding without mut, and the value stays 1600.

Practice tickets

  1. RUST-401 · Receipts for three or more bags show the discounted price as full → Free
  2. RUST-402 · Mushroom buyers pay for the paper bag → Sign in · Pro
  3. RUST-403 · Every soup is sold at the soup-of-the-day price → Sign in · Pro

Keep exploring

Rust ownership and moves →

Primary references