Variables and Mutability

Immutable Variables

A variable declared with let is immutable by default. Once a value is bound to the name, the name cannot be assigned a different value, and the value cannot be modified through that name. To change a variable, it must be declared with mut, which is covered in the next section.

// ❌ does not compile
fn main() {
    let x = 5;
    println!("x = {x}");
    x = 6;
    println!("x = {x}");
}
error[E0384]: cannot assign twice to immutable variable `x`
 --> main.rs:5:5
  |
3 |     let x = 5;
  |         - first assignment to `x`
4 |     println!("x = {x}");
5 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
3 |     let mut x = 5;
  |         +++

The error is reported at compile time. A program that reassigns an immutable variable never builds.

What Immutable Means

Immutability is a property of the binding, not only of the value. A binding is immutable when the compiler forbids both of these through it:

  • assigning a new value (x = 6, x += 1)
  • taking a mutable reference (&mut x) or calling a method that needs one (x.push(4))
// ❌ does not compile
fn main() {
    let numbers = vec![1, 2, 3];
    numbers.push(4);
    println!("{numbers:?}");
}
error[E0596]: cannot borrow `numbers` as mutable, as it is not declared as mutable
 --> main.rs:4:5
  |
4 |     numbers.push(4);
  |     ^^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
3 |     let mut numbers = vec![1, 2, 3];
  |         +++

Vec::push takes &mut self. Calling it needs a mutable borrow of numbers, and an immutable binding does not allow one. The same applies to String::push_str, HashMap::insert, and every other method that mutates its receiver.

Taking an explicit &mut borrow fails for the same reason:

// ❌ does not compile
fn main() {
    let x = 5;
    let y = &mut x;
    *y += 1;
}
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
 --> main.rs:4:13
  |
4 |     let y = &mut x;
  |             ^^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
3 |     let mut x = 5;
  |         +++

Immutability Is Deep for Owned Values

If a variable owns a struct, array, tuple, or collection, none of its parts can be changed either:

// ❌ does not compile
struct Point {
    x: i32,
    y: i32,
}

fn main() {
    let p = Point { x: 1, y: 2 };
    p.x = 10;
    println!("{} {}", p.x, p.y);
}
error[E0594]: cannot assign to `p.x`, as `p` is not declared as mutable
 --> main.rs:9:5
  |
9 |     p.x = 10;
  |     ^^^^^^^^ cannot assign
  |
help: consider changing this to be mutable
  |
8 |     let mut p = Point { x: 1, y: 2 };
  |         +++

Rust has no way to mark one field of a struct as mutable while the rest are not. Mutability belongs to the whole binding.

Immutable References

A reference created with & gives read-only access to the value it points to. Writing through it is an error, even when the original variable is declared mut:

// ❌ does not compile
fn main() {
    let value = 10;
    let reference = &value;
    *reference += 1;
    println!("{value}");
}
error[E0594]: cannot assign to `*reference`, which is behind a `&` reference
 --> main.rs:5:5
  |
5 |     *reference += 1;
  |     ^^^^^^^^^^^^^^^ `reference` is a `&` reference, so it cannot be written to
  |
help: consider changing this to be a mutable reference
  |
4 |     let reference = &mut value;
  |                      +++

Any number of immutable references to the same value can exist at once, because none of them can change it:

fn main() {
    let data = vec![1, 2, 3];
    let sum: i32 = data.iter().sum();
    let reader = &data;
    let again = &data;
    println!("{sum} {} {}", reader.len(), again.len());
}
6 3 3

While an immutable reference is alive, the value it points to cannot be modified through another path. This rule is the basis of borrowing:

// ❌ does not compile
fn main() {
    let items = vec![1, 2, 3];
    let first = &items[0];
    items.push(4);
    println!("{first}");
}
error[E0596]: cannot borrow `items` as mutable, as it is not declared as mutable
 --> main.rs:5:5
  |
5 |     items.push(4);
  |     ^^^^^ cannot borrow as mutable
  |
help: consider changing this to be mutable
  |
3 |     let mut items = vec![1, 2, 3];
  |         +++

error[E0502]: cannot borrow `items` as mutable because it is also borrowed as immutable
 --> main.rs:5:5
  |
4 |     let first = &items[0];
  |                  ----- immutable borrow occurs here
5 |     items.push(4);
  |     ^^^^^^^^^^^^^ mutable borrow occurs here
6 |     println!("{first}");
  |                ----- immutable borrow later used here

Two errors appear: the binding is not mut, and even with mut the push would conflict with the live reference first.

Interior Mutability

Some standard library types, such as Cell and RefCell, let a value change through a shared, non-mut binding. This is called interior mutability. The type itself enforces the safety rules, either by copying values in and out or by checking borrows at runtime.

use std::cell::Cell;

fn main() {
    let counter = Cell::new(0);
    counter.set(counter.get() + 1);
    counter.set(counter.get() + 1);
    println!("{}", counter.get());
}
2

counter is an immutable binding, yet its contents changed. Cell::set takes &self, not &mut self, so the compiler's rule about immutable bindings is not violated. Immutability, then, means "no mutation through ordinary assignment and &mut borrows", not "the bytes never change". Interior mutability is the exception and is used deliberately.

Immutable Does Not Mean Constant

An immutable variable can be initialized from a value that is only known while the program runs: user input, a function result, a file's contents. It is fixed after initialization, not at compile time.

fn main() {
    let args: Vec<String> = std::env::args().collect();
    let argument_count = args.len();
    println!("{argument_count}");
}
1

A const item is different: its value must be computable at compile time, and it is inlined where used. Constants have their own section.

Initializing Once After Declaration

An immutable variable may be declared first and given its value later. The compiler allows exactly one assignment on every path through the code:

fn main() {
    let total: i32;
    total = 5;
    println!("{total}");

    let label;
    if total > 3 {
        label = "big";
    } else {
        label = "small";
    }
    println!("{label}");
}
5
big

label is assigned in both branches of the if, and exactly once on each path. A second assignment on the same path is rejected:

// ❌ does not compile
fn main() {
    let total: i32;
    total = 5;
    total = 6;
    println!("{total}");
}
error[E0384]: cannot assign twice to immutable variable `total`
 --> main.rs:5:5
  |
4 |     total = 5;
  |     --------- first assignment to `total`
5 |     total = 6;
  |     ^^^^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
3 |     let mut total: i32;
  |         +++

Deferred initialization is covered in the section on uninitialized variables.

Where Immutability Applies

Every binding is immutable unless it is declared with mut. That includes bindings that are not created by a plain let.

Function Parameters

// ❌ does not compile
fn increment(value: i32) {
    value += 1;
}

fn main() {
    increment(1);
}
error[E0384]: cannot assign to immutable argument `value`
 --> main.rs:3:5
  |
3 |     value += 1;
  |     ^^^^^^^^^^ cannot assign to immutable argument
  |
help: consider making this binding mutable
  |
2 | fn increment(mut value: i32) {
  |              +++

Tuple and Pattern Bindings

Each name introduced by a pattern is its own binding and is immutable unless marked mut:

// ❌ does not compile
fn main() {
    let (a, b) = (1, 2);
    a = 10;
    println!("{a} {b}");
}
error[E0384]: cannot assign twice to immutable variable `a`
 --> main.rs:4:5
  |
3 |     let (a, b) = (1, 2);
  |          - first assignment to `a`
4 |     a = 10;
  |     ^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
3 |     let (mut a, b) = (1, 2);
  |          +++

Loop Variables

The variable in a for loop is a new binding on every iteration, and it is immutable:

// ❌ does not compile
fn main() {
    let items = [1, 2, 3];
    for item in items {
        item += 1;
    }
}
error[E0384]: cannot assign twice to immutable variable `item`
 --> main.rs:5:9
  |
4 |     for item in items {
  |         ---- first assignment to `item`
5 |         item += 1;
  |         ^^^^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
4 |     for mut item in items {
  |         +++

A variable declared with let inside the loop body is also created fresh each iteration, so it can be immutable even though the loop runs many times:

fn main() {
    for i in 0..3 {
        let squared = i * i;
        println!("{i} -> {squared}");
    }
}
0 -> 0
1 -> 1
2 -> 4

Moving a Value Into a Mutable Binding

Immutability belongs to the binding, not to the value. When ownership of a value moves to a new binding, the new binding decides whether the value can be changed. The original binding can no longer be used.

fn main() {
    let a = vec![1, 2, 3];
    let mut b = a;
    b.push(4);
    println!("{b:?}");
}
[1, 2, 3, 4]

a was immutable, b is mutable, and the same vector is modified through b. This is allowed because a has given up ownership, so nothing else can observe the change.

Immutability Is Not Shadowing

Declaring a new variable with the same name does not mutate the old one. It creates a second binding that hides the first:

fn main() {
    let x = 5;
    let x = x + 1;
    println!("{x}");
}
6

The first x is never modified. Shadowing is covered in its own section.

Why Variables Are Immutable by Default

Benefit Explanation
Easier reasoning A reader sees let x = 5; and knows x is 5 everywhere it is used, with no need to scan for assignments
Intent is visible The word mut marks exactly the variables that change, so changing state stands out
Fewer bugs Accidental reassignment is a compile error instead of a runtime logic bug
Safe sharing A value that cannot change can be read from many places, and from many threads, without data races
Optimization The compiler knows an immutable value is stable and can keep it in a register or fold it into later computations

Using mut is a deliberate, local decision: it is added to the single variable that needs to change and nowhere else.