Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Values, Names, and the “let” Word

If you only remember one thing: in Rust, a name points at a value, and that value does not change unless you wrote mut on purpose.

Naming a value

You give a value a name with let:

fn main() {
    let age = 30;
    println!("age is {age}");
}

▶ Run this in the Rust Playground

Reading: “let age be 30”. The arrow only goes one way. age is bound to 30. The binding is the important word.

Now try to change it:

fn main() {
    let age = 30;
    age = 31; // won't compile
    println!("age is {age}");
}

Rust will refuse to compile this. The error is short and clear:

error[E0384]: cannot assign twice to immutable variable `age`

Why “immutable by default”

The binding card

let vs let mut

let age = 30; locked. you cannot write to it. let mut age = 30; unlocked. age = 31; will work.
Immutable is the default. Mutability is something you have to ask for, in writing.

Every language eventually learns the same lesson: the bugs that are hardest to find are the ones where a value changed somewhere you were not looking. Rust’s answer is “fine, then values do not change unless you explicitly say so.” You opt in with mut:

fn main() {
    let mut age = 30;
    age = 31;
    println!("age is {age}");
}

▶ Run this in the Rust Playground

Plain English

In JavaScript const and let, it's the exception that something should not change. In Rust, it's the rule. You write mut only when you actually need to change the value, which turns out to be less often than you think.

Types are usually inferred

Notice we did not write a type for age. Rust looked at 30 and decided age is an i32 — a 32-bit signed integer. You can spell it out when you want:

#![allow(unused)]
fn main() {
let age: u8 = 30;       // 0..=255
let price: f64 = 9.99;  // a double-precision float
let name: &str = "Ada"; // borrowed string slice
let ok: bool = true;
}

The types you will see in the first month:

  • Integers: i8 i16 i32 i64 i128 (signed), u8 u16 u32 u64 u128 (unsigned). Default: i32.
  • Floats: f32, f64. Default: f64.
  • Booleans: bool.
  • Characters: char — one Unicode scalar, written 'a'.
  • Strings: &str (a borrowed view of text) and String (an owned, growable string). We will make this distinction real in Chapter 5.

Shadowing: a reused name, not a changed value

This compiles, and it is a common Rust pattern:

fn main() {
    let spaces = "   ";
    let spaces = spaces.len();
    println!("{spaces}");
}

▶ Run this in the Rust Playground

spaces was a string. Now, by a second let, it is a number. The old binding is shadowed by the new one. No value was mutated — a new binding just took the name.

Analogy

Imagine a whiteboard label. let mut is a label on a slot: you can replace what is in the slot. Shadowing is the label itself moving to a new slot. The old slot is still there, you just stopped caring about it.

Watch what the compiler does with each of these bindings — including the one it refuses.

Interactive simulation (requires JavaScript): assigning to an immutable binding fails with E0384 before the program ever runs; declaring the binding with mut allows the stack slot to be updated in place.

## Try this
Three-minute exercises
  1. Make a let mut counter = 0;, then increment it three times, then print it.
  2. Make an immutable let pi: f64 = 3.14; and try to change it. Read the error carefully.
  3. Use shadowing to change let age = "30"; into a number, using let age: u32 = age.parse().unwrap(); Run it.

Next: the four shapes every Rust program uses to organize data — struct, enum, tuple, array.

The shape of data →