Start learning free

LESSON 14 OF 414 · VARIABLES

let, mut and Shadowing in Rust: Changing Values Safely

Lesson Read the full explanation

In most languages, a variable can be changed whenever you like. Rust flips that default: every let binding is immutable — once you give it a value, that value is final. This is not a limitation, it's a safety feature: if a value is never supposed to change, the compiler now guarantees it never does, and whole categories of bugs disappear before the program even runs.

When you genuinely need a value to change — a counter, a running total — you opt in to mutation by writing let mut:

let mut score = 0;
score += 10;    // OK — score was declared with mut
println!("{}", score);

The trap:

Sooner or later, every Rust beginner writes an assignment to a plain let binding. The compiler stops you with this exact error — read it carefully, because it also tells you the fix:

error[E0384]: cannot assign twice to immutable variable `clicks`
 --> src/main.rs:3:5
  |
2 |     let clicks = 0;
  |         ------ first assignment to `clicks`
3 |     clicks += 1;
  |     ^^^^^^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut clicks = 0;
  |         +++

The help: line is the compiler literally handing you the solution: add mut.


Shadowing — rebinding without mutating:

There is a second way to "change" a variable that needs no mut at all: write a new let with the same name. This is called shadowing. It doesn't modify the old value — it creates a fresh binding that hides the old one. Because it's a brand-new binding, it can even have a different type:

let input = "7";                          // input is a &str (text)
let input: i32 = input.parse().unwrap();  // input is now an i32 (number)
println!("{}", input + 1);                // prints 8

.parse() converts text into a number; the : i32 annotation tells it which kind of number, and .unwrap() takes the value out (fine here, since we know the text is numeric). Mutation with mut can never change a variable's type — shadowing is the idiomatic way to convert a value and keep its name.


Assignment
Follow the Coding Tutorial to build a tiny click counter, then convert a text label into a number:

  • Declare a mutable counter clicks starting at 0, increment it twice with +=, and print it.
  • Declare label as the string "42", shadow it into an i32 using .parse().unwrap(), and print its double.

The exact expected output is:

clicks = 2
label doubled = 84

Hint
If the compiler says cannot assign twice to immutable variable, you forgot mut. If it asks about a type it "cannot infer", give the new binding an explicit : i32.


STEP 1 OF 7
    Ctrl + Enter to check

    RUST · VARIABLES

    Let Mut And Shadowing in Rust, explained

    Bindings are immutable by default

    In many languages you can reassign a variable whenever you like. In Rust, a let binding is immutable: once it has a value, that value is final. This is a safety feature. If a value never changes, then the compiler guarantees it, and a whole category of bugs can't happen.

    When a value genuinely has to change, such as a counter or a running total, you opt in with let mut:

    let mut score = 0;
    score += 10;
    println!("{}", score);
    

    This prints 10. The += operator adds to the current value and stores the result back in the same variable.

    The classic error

    Sooner or later every Rust beginner assigns to a plain let binding:

    let clicks = 0;
    clicks += 1;
    println!("{}", clicks);
    

    The program doesn't compile. The compiler reports error[E0384]: cannot assign twice to immutable variable `clicks`. It marks the declaration as the first assignment, marks the += line as the second, and ends with help: consider making this binding mutable, followed by the corrected line let mut clicks = 0;. The message tells you the fix, so read to the end of it.

    Shadowing: a new binding with the same name

    There is a second way to give a name a new value, and it needs no mut: write another let with the same name. This is called shadowing. It doesn't change the old value. It creates a fresh binding that hides the old one for the rest of its scope.

    let x = 5;
    let x = x + 1;
    {
        let x = x * 2;
        println!("inner {}", x);
    }
    println!("outer {}", x);
    

    This prints inner 12 and then outer 6. The second let makes x equal to 6. The block creates a third x equal to 12, which disappears at the closing brace, so the outer x is 6 again.

    Shadowing can change the type

    Because a shadowing let is a brand-new binding, it can have a different type from the old one. A mut variable can never do that:

    let mut label = "42";
    label = 42;
    

    The compiler rejects this with error[E0308]: mismatched types and expected `&str`, found integer. A mut variable keeps the type it started with. Shadowing is the usual way to convert a value and keep its name:

    let input = "7";
    let input: i32 = input.parse().unwrap();
    println!("{}", input + 1);
    

    It prints 8. The first input is text. The second is a number made from it, and the text version is no longer reachable.

    How parse and unwrap work

    parse() turns text into another type. It needs to know which one, so the new binding is annotated with : i32. Without the annotation, let label = label.parse().unwrap(); fails with error[E0284]: type annotations needed.

    Text isn't always a number, so parse() returns a Result that is either a value or an error. "abc".parse::<i32>() gives Err(ParseIntError { kind: InvalidDigit }). The call to unwrap() takes the value out of a successful result, and it panics, stopping the program, on an error. Using it here is fine because the text is known to be digits.

    Solving the exercise

    The expected output is two lines, clicks = 2 and label doubled = 84. The steps follow the two lessons above:

    1. Declare the counter with let mut clicks = 0;, since it will change.
    2. Increase it twice with clicks += 1;, on two lines. No new let, because it is the same binding.
    3. Print it with println!("clicks = {}", clicks);. The text before the placeholder is part of the output.
    4. Declare label as the text "42". The digits in quotes are a &str, not a number.
    5. Shadow it with let label: i32 = label.parse().unwrap();. The right side still refers to the old, text label; the new one takes over from the next line.
    6. Print the double with println!("label doubled = {}", label * 2);. The multiplication works because label is now an i32.
    let mut clicks = 0;
    clicks += 1;
    clicks += 1;
    println!("clicks = {}", clicks);
    let label = "42";
    let label: i32 = label.parse().unwrap();
    println!("label doubled = {}", label * 2);
    

    The output is:

    clicks = 2
    label doubled = 84
    

    When to use which

    • Use let by default. Most values never change.
    • Use let mut when the same variable has to be updated several times, as in a counter.
    • Use shadowing to convert a value to another type, or to do a step of preparation and keep the name.

    Key takeaways

    • let bindings are immutable, and assigning to one gives cannot assign twice to immutable variable.
    • let mut allows reassignment, but the type stays the same.
    • Shadowing declares a new binding with the same name, and it may have a different type.
    • Parse text into a number with parse(), an explicit type, and unwrap() when you know it will succeed.
    Keep going with RustAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free