---
title: 'Variables and Immutability'
source: 'https://academia.sh/en/courses/rust-fundamentals/variables-and-immutability'
course: 'Rust Fundamentals'
language: en
updated: '2026-09-15T04:58:45+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Shows that a let binding is immutable by default and that the assignment itself produces ()'
    - 'Compares shadowing and mut-based reassignment as two separate mechanisms that reach the same numeric result'
    - "Distinguishes that shadowing can change a binding's type while mut-based reassignment keeps it"
---

# Variables and Immutability

let is a binding statement and the assignment itself produces (); the same name being rebound three times (shadowing) and bound once and changed twice (mut) reach the same numeric result, but only shadowing can change the type along the way — because one sets up a new binding each time, the other writes over an existing binding.

The entry point's body can now be descended into, and most of the first lines of that body
bind a value to a name — a binding. This lesson measures the binding itself from two angles:
whether the binding operation itself is an expression or a statement, and why Rust's two
separate ways of rebinding the same name — shadowing, which sets up a brand-new binding each
time, and `mut`, which writes over an existing binding — behave so differently from one
another while reaching the same result.

The Programming Fundamentals course built the variable definition with Python, and defined
shadowing there too. These concepts are not rebuilt here; this lesson's share is that Rust
sets up a binding as **immutable** by default, and that shadowing opens a door through this
immutability.

This ordering is not arbitrary. The previous lesson established the smallest set required for
a source to run; this lesson descends into that set, into the entry point's body, and asks how
a name binds to a value there. The next lesson will ask what type this bound value can be —
but before asking what a type is, it is necessary to know how a name holds on to a value of
that type, and how tight that hold is. Immutability is the name of that tightness.

## Binding Is a Statement

The writing `let name = value;` splits into two separate parts. The right side of the equals
sign is an expression — it is evaluated and produces a value. The whole line, the binding
starting with `let` itself, is a statement: it can never be used as a value anywhere. This
distinction looks small, but its consequences are concrete — you cannot pass a binding as an
argument to a function, add it to a list, or embed it inside another expression, because all
you have in hand is `()`.

**LB8.** In Rust, `let` only stands at the body level and cannot be placed inside an
expression; a writing like `(let x = 5)` is not accepted by the compiler. This is the concrete
counterpart of `let` "always being a statement" — it has no exception, it never turns into a
value in any context. One consequence of this is that a reader of the source can track where a
new name is established from a single place: every new binding opens at the start of a line,
with `let`; there is no need to search for a binding hidden in the middle of an expression.

We saw in the previous lesson's `expression.rs` core that the assignment itself falls into the
same class too (the "the assignment itself" row printed `()`). The same fact is measured again
here, with a small example specific to the subject of variables.

```rust
// assignment_as_statement.rs — the assignment itself produces no value
fn main() {
    let mut x = 1;
    println!("x at the start: {}", x);
    let assignment_result = { x = 9; };
    println!("x now: {}", x);
    println!("the assignment's own value: {:?}", assignment_result);
}
```

```
x at the start: 1
x now: 9
the assignment's own value: ()
```

`x = 9` is evaluated as an expression — `x`'s new value is written to memory — but this
expression's own value is `()`. The binding named `assignment_result` holds this `()`, not the
`9`. The only way to reach `x`'s new value is to read `x` itself; reading the assignment's
result never works, because there is no value there to read.

This completes an observation from the previous lesson: `if`, `match`, `loop`, and a block are
each a value, but `let` and assignment are not values. Two groups of writings, one saying
"this is a value" and the other "this is not a value," standing together in the same language
is a deliberate choice. A branch or a loop producing a value lets that writing's result be
carried directly into a binding; a binding itself producing a value would be meaningless,
because a binding is not itself a result, it is the act of storing a result. Producing a value
and allocating a place and writing to it remain two separate acts in the source, and the
classification does not mix the two.

## Shadowing and mut: Same Result, Two Different Paths

There are two ways to rebind the same name in Rust. One is **shadowing**: the `let` keyword is
written again and a brand-new binding is set up; the old binding can no longer be reached by
that name, but it has not vanished. The other is `mut`: the binding is set up once with
`let mut`, and later lines write directly over it with `=`; there is no new binding here, only
the value the existing binding holds changes. The measurement below runs the two side by side, applying the
same two steps (add three, multiply by two) to each.

```rust
// binding.rs — the difference between shadowing and mut
fn main() {
    // shadowing: the same name is bound three times, each one a new binding
    let value = 5;
    let value = value + 3;
    let value = value * 2;
    println!("shadowing result: {}", value);

    // mut: bound once, changed twice (writing to the same binding)
    let mut counter = 5;
    counter = counter + 3;
    counter = counter * 2;
    println!("mut result: {}", counter);

    println!("equal: {}", value == counter);
}
```

```
shadowing result: 16
mut result: 16
equal: true
```

Both paths reach `16`, but even though they land in the same place, the way they get there
differs. On the shadowing side there are three separate `value` bindings — the second reads
the first and sets up a new binding, the third reads the second and sets up yet another new
one. On the `mut` side there is a single `counter` binding; both assignment lines write over
that same binding, setting up no new binding.

The question distinguishing these two paths is this: does the next line **read** the name the
previous line established, or does it **write** to the place the previous line established? In
shadowing, every `let value` line first evaluates the expression on the right (using the
previous `value`), then places this result into an entirely new binding — reading and setting
up a new binding are two separate steps. On the `mut` side, reading and writing happen on the
same binding; the line `counter = counter + 3` first reads `counter`'s current value, then
writes the new value to that same binding. The result in both is one addition and one
multiplication, but one reuses the name three times while the other never lets go of the same
name.

**LB9.** In shadowing the old binding is not "deleted," it only becomes unreachable by its
name. The first `value` (`5`) and the second `value` (`8`) can still exist somewhere in
memory, but no name in the source remains to reach them — with the third `let value`, that
name moved to the final binding. This behavior concerns memory management, the subject of the
Ownership and the Memory Model course; here what is observed is only this: shadowing reuses
the name, not the binding.

Shadowing reusing the name connects with another fact from an earlier lesson: a block is
itself an expression and carries its own scope. Shadowing inside a block loses its effect once
the block closes.

```rust
// shadowing_scope.rs — shadowing stays within its scope
fn main() {
    let value = 10;
    let inside = {
        let value = value * 100;
        value
    };
    println!("shadowing inside the block: {}", inside);
    println!("the outside value did not change: {}", value);
}
```

```
shadowing inside the block: 1000
the outside value did not change: 10
```

The inner block's `let value = value * 100;` reads the outer `value` and sets up a new binding
in its own scope; this new binding is valid only inside the block. When the block closes, the
outer `value` comes back completely untouched — the inner binding never wrote over the outer
binding, it only borrowed the same name within its own narrow scope. This is the second point
where shadowing departs from `mut`: a change made with `mut` recognizes no scope boundary, it
is visible everywhere the same binding exists; shadowing, with every `let`, sets up both a new
binding and that binding's scope anew.

## Shadowing Can Change the Type

The difference between these two paths is not only mechanical, it is also a difference in
capability. Because shadowing sets up a brand-new binding every time, the new binding's type
does not have to be the same as the old one's.

```rust
// shadowing_type.rs — shadowing can change the same name's type
fn main() {
    let value = 21;
    let value = value * 2;
    let value = format!("result={}", value);
    println!("{}", value);
}
```

```
result=42
```

The first `value` is an integer, the third `value` is a string (`String`). All three carry the
same name, but all three are independent bindings; the last one does not have to know the
previous one's type, it only reads its value and sets up a new binding of a new type.

If the same writing is tried with `mut`, the compiler rejects it: after the line
`let mut value = 21`, a line trying to assign the result of a string-formatting call to
`value` does not compile, because `mut` does not set up a new binding, it only allows the
existing binding to be replaced with another value of the **same type**. Once a binding is set
up as `i32`, a `String` cannot be written to that binding — to the compiler, this is
equivalent to trying to fit two differently sized values into the same place. Shadowing does
not carry this restriction, because every `let` tells the compiler "allocate a new place
here"; `mut` says "write to the existing place," and that place's shape is fixed by the first
`let`.

This difference shows why shadowing is not merely a shortcut. In a chain of transformations
carrying the same name (such as parsing a string into a number, then turning that number into
a formatted string), it lets each step stay in its own natural type; a reader reads which type
applies at each step from the binding's most recent `let` line, without having to track the
type of intermediate steps. In the `shadowing_type.rs` example, this chain had three steps: it
started as an integer, was doubled as an integer, and was formatted as a string. If the same
chain were to be written with `mut`, either three separate names would be required
(`raw_value`, `doubled_value`, `formatted_value`), or an extra conversion step would be needed
to keep every step in the same type. Shadowing makes both of these options unnecessary: a
single name follows the logical steps of the transformation, while letting each step carry its
own type.

This capability also has a cost: tracking how many times the same name has been shadowed in a
row demands more attention from a reader than `mut` does. Because `mut` never changes a
binding's type, when a reader sees `counter` in the middle of the code, the type is clear from
the first `let mut`. In shadowing, giving the same guarantee requires going back to the
nearest `let value` line — a `value` three lines earlier may carry a different type. For the
rest of the course, shadowing will usually be used between short, nearby lines; this closeness
keeps type tracking from getting lost while reading.

## Limiting Measurement: Immutability Is a Binding's Boundary

The previous two sections always showed immutability through a single value — an integer or a
string. It needs clarifying how immutability behaves on a value that holds several components
together. A binding set up without `mut` protects not only its own value but all of that
value's inner fields — you cannot separately change even a single component of a tuple.

```rust
// immutability_boundary.rs — the state of inner fields while the binding is immutable
fn main() {
    let point = (3, 4);
    println!("start: ({}, {})", point.0, point.1);

    let point2 = (point.0 + 1, point.1);
    println!("new binding: ({}, {})", point2.0, point2.1);
    println!("old binding unchanged: ({}, {})", point.0, point.1);
}
```

```
start: (3, 4)
new binding: (4, 4)
old binding unchanged: (3, 4)
```

`point` was set up with no `mut` at all. To "update" it, `point.0 = 4;` would have to be
written — this line does not compile, because `point` is an immutable binding, and this
immutability applies to every component of the tuple at once, not just the name visible from
outside. Instead, the code set up a new binding (`point2`) and read two of the old one's
components to compute a third value; `point`, as seen in the output's last line, never
changed. The new binding was set up not over the old one, but next to it; the two bindings
continue to exist at the same time, independent of one another.

**LB10.** This can read like "immutability is deep," but that is not the full picture. What is
shown here is that a binding **itself** is immutable — no component belonging to that binding
can be rewritten without `mut`. But whether the same data can be shared through another
binding or reference, and if shared, whether immutability holds from there too — that is the
question of ownership and borrowing rules, and it is the subject of the Ownership and the
Memory Model course. The boundary drawn here is this: a binding not taking `mut` guarantees no
change is allowed through that binding's own name; it is not a broader guarantee about the
whole of the data.

This boundary belongs to the same family as the previous two lessons' limiting measurements.
In Rust's Design Goals, a method's class was determined by looking at its signature, not its
call site; in Setup and the Package Manager, a binary's name was determined by looking at the
manifest, not the source. A similar pattern holds here: how "immutable" a binding is gets
determined only by looking at that binding's own declaration (whether `mut` is present or not)
— without looking at how the data is shared elsewhere. All three lessons repeat the same
lesson: when reading a guarantee in Rust, knowing **where** the guarantee comes from is as
important as the guarantee itself.

## Summary

- `let name = value;` is a statement and can never be used as a value anywhere; the expression
  on the right is evaluated and bound to the name.
- An assignment itself also produces `()`; the only way to reach the new value is to read the
  name.
- Shadowing sets up a brand-new binding with every `let`; reassignment with `mut` writes over
  the existing binding. Both can reach the same numeric result.
- Only shadowing can change a binding's type, because a new place is allocated at every step;
  `mut` stays bound to the binding's initial type.
- A binding being immutable covers all of that binding's inner components, but it does not
  answer the question of whether the data can be shared some other way.
- Shadowing is bounded by scope: shadowing inside a block disappears when the block closes,
  without ever affecting the outer binding; a change made with `mut` recognizes no scope.

## Next Step

The values bindings carry have not yet been examined one by one — only integers have been
seen. The next lesson descends into Rust's primitive types: integer, floating-point, boolean,
character, and tuple, and shows by running it what value each produces as an expression, and
why `()` counts as one of these types too. The binding concept established in this lesson will
not change there — only the variety of bound values will expand, and each type's own
boundaries (the range an integer can overflow into, a tuple's fixed length) will be measured
one by one.
