Variables¶
Level: 101 · for newcomers
One line: let binds a name to a value, and the binding is immutable by default — mut is not a convenience you sprinkle on, it is a promise to the next reader that this name changes.
That is the whole of the syntax. The annotation is optional — let x = 10; is the same program — and what a type annotation does is its own page.
The second line is the lesson¶
error[E0384]: cannot assign twice to immutable variable `x`
--> immutable.rs:3:5
|
2 | let x: i32 = 10;
| - first assignment to `x`
3 | x = 20;
| ^^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable
|
2 | let mut x: i32 = 10;
| +++
x is not a box holding 10 that you may refill. It is a name for a value, and by default the name is spoken for. Note what the compiler offers: not "use a different name" but "make this binding mutable" — the fix is one word, and the word is a decision.
mut is the permission assignment needs¶
Nothing else changes. mut does not make the value special, it makes the name writable.
And the promise is enforced in both directions — a mut nothing ever writes to is a warning:
warning: variable does not need to be mutable
--> unused_mut.rs:2:9
|
2 | let mut total = 0;
| ----^^^^^
| |
| help: remove this `mut`
|
= note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default
So mut in Rust source means this changes below, reliably, because the compiler will not let it mean anything weaker. That is worth more than the mutation it permits: scanning a function for mut tells you which of its names are moving parts. What a warning is asking reads this one and its neighbours in full.
Mutability belongs to the binding, not to the value¶
let owned = String::from("Ada"); // not mut
let mut moved = owned; // now it is
moved.push_str(" Lovelace");
println!("{moved}"); // Ada Lovelace
The same heap bytes were immutable under one name and writable under the next, and nothing about the String changed. This is the sentence that stops mut being read as "this data is mutable", and a name is not a place proves it properly, with the borrow checker rather than with addresses.
A binding is scoped to its block¶
let outer = "visible to the end of main";
{
let inner = "visible only inside these braces";
println!("{inner}");
}
println!("{outer}");
// println!("{inner}"); // error[E0425]: cannot find value `inner` in this scope
The compiler's help line on that error is unusually good — it says the binding exists in a different scope in the same function and points at it, rather than assuming a typo. Braces are the unit: a block is an expression is the same braces doing their other job, and scope is about names, not values is what "goes out of scope" actually means once a value can be moved out of one.
let again is a new variable, not an assignment¶
No mut, and no error — because nothing was assigned. A second variable was declared and took over the name, and it may have a different type. That is shadowing, it is a large enough topic to have its own map, and the distinction from mut is not stylistic: a shadow cannot write to anything that outlives its block. Start at the shadowing map.
| you write | what happens | needs mut? |
may change the type? |
|---|---|---|---|
x = 20; |
writes into the existing place | yes | no |
let x = 20; |
new variable, same name | no | yes |
If you are coming from another language¶
Python. x = 10 then x = 20 is legal and unremarkable, and the closest thing to let is nothing at all — assignment declares. Two habits transfer badly. First, Python has no immutable binding: CONSTANT = 3 is a naming convention the interpreter never checks, and Final[int] is a hint for a type checker, not for CPython. Rust's default is the enforced version of what UPPER_CASE was always trying to say. Second, Python's x = 20 after x = "text" is fine, so the mental model is a name points at whatever you last put there; in Rust that line is a type error before it is anything else, and the rebinding you actually want is a second let.
The reverse direction is the one that stings: because Python names are freely rebindable, Python programmers reach for reassignment where Rust wants a fresh binding — accumulating into a variable declared far above, rather than computing a value and naming it once. The idiomatic Rust of a for loop that builds a total is often not a mut at all but an iterator chain, so a Python-shaped Rust function tends to carry muts that a Rust reviewer reads as "something changes here" and then cannot find.
| Python | Rust | |
|---|---|---|
| declare | x = 10 |
let x = 10; |
| change the value | x = 20 |
let mut x = 10; x = 20; |
| reuse the name for a new type | x = "text" |
let x = "text"; (a new variable) |
| a name that must not change | X = 10 by convention |
the default |
ABAP. DATA lv_x TYPE i. then lv_x = 10. is the two-step Rust collapses into one, and everything declared with DATA is writable forever — the only immutable thing in the language is CONSTANTS, which must be given its value at declaration and can never be assigned again. So the mapping is clean but inverted:
| ABAP | Rust | |
|---|---|---|
| ordinary variable | DATA lv_x TYPE i. |
let mut x: i32; |
| never changes | CONSTANTS lc_x TYPE i VALUE 10. |
let x = 10; — the default |
| inferred declaration | DATA(lv_x) = 10. |
let x = 10; |
What changes is which one you have to type. ABAP's default is mutable and immutability costs a different keyword and a compile-time-only value, so CONSTANTS is reserved for genuine constants; Rust's default is immutable and mutability costs the extra word, so a Rust function routinely has a dozen names that never change and no ceremony announcing it. Reading Rust, the useful reflex is the opposite of the ABAP one: do not scan for what is constant, scan for the three muts.
Worth knowing for the DATA(lv_x) = 10. row: ABAP's inline declaration takes its type from the right-hand side of that statement, and nothing later can influence it. Rust's inference reads the whole function — see type inference, where that difference is the point.
The verified output¶
Verified output of variables.rs — regenerated by tools/run_examples.py, never hand-typed.
1. `let` binds a name to a value
let x: i32 = 10; x = 10 i32
let y = 10; y = 10 i32
Same program. The annotation is a check, not a requirement —
nothing here needed it, because 10 has a fallback type.
2. The binding is immutable by default
Adding a second line does not work:
let x: i32 = 10;
x = 20; // error[E0384]: cannot assign twice to immutable variable
`x` is not a box holding 10 that you may refill. It is a name for
a value, and the name is spoken for.
3. `mut` is the permission that assignment needs
let mut count = 0; count = 0
count = 1; count = 1
count += 1; count = 2
One word, and every later line may write to it.
4. `mut` you did not use is a warning, not a shrug
let mut total = 0; // never assigned again
println!("{total}");
warning: variable does not need to be mutable
The compiler holds you to the promise: `mut` says *this changes*,
so a `mut` nothing writes to is a claim the code does not keep.
5. Mutability belongs to the binding, not to the value
let owned = String::from("Ada"); // not mut
let mut moved = owned; // now it is
moved.push_str(" Lovelace"); moved = "Ada Lovelace"
The same bytes were immutable under one name and writable under
the next. Nothing about the String changed — only the binding.
6. A binding is scoped to its block
inner: visible only inside these braces
outer: visible to the end of main
println!("{inner}"); // error[E0425]: cannot find value `inner`
7. `let` again is a new variable, not an assignment
let spaces = " "; &str
let spaces = spaces.len(); usize spaces = 3
That is shadowing, and it needed no `mut` — because nothing was
assigned. A second variable took over the name.
The rule
`let` names a value; `mut` lets a later line write through the name;
`let` again replaces the name and may change its type.
Run it yourself:
rustc --edition 2024 15_First_Programs/variables/examples/variables.rs -o /tmp/variables && /tmp/variables
See also¶
- Values — what you can put on the right of a
letbefore defining a type of your own - Type inference — how the compiler decides what
let x = 10;holds - What a type annotation does — the
: i32, and the four shapes where it stops being decoration - The shadowing map —
lettwice, and the five questions it raises - A name is not a place — the proof that
mutand a shadow are different mechanisms - What a warning is asking —
unused_mutand its neighbours, read properly - Variables and mutability ↗ ·
E0384↗ · Comprehensive Rust: Variables ↗
Po polsku¶
Polszczyzna trochę tu przeszkadza: „zmienna” znaczy dosłownie taka, która się zmienia, więc zmienna, której zmienić nie wolno, brzmi jak pomyłka. A dokładnie to robi let — wiąże nazwę z wartością i domyślnie jest to wiązanie (binding) niemutowalne. Druga linijka jest tu całą lekcją: x = 20; po let x: i32 = 10; to E0384, cannot assign twice to immutable variable. Warto przeczytać, co kompilator proponuje, bo proponuje jedno słowo, a nie inną nazwę: consider making this binding mutable, z mut dorysowanym pod spodem. Nie ulegaj przy tym pokusie tłumaczenia let x = 10; jako „stałej” — stała to const X: i32 = 10;, wymaga typu i wartości znanej w czasie kompilacji, podczas gdy let nazywa zwykłą wartość obliczoną w czasie działania i żyjącą w swoim zasięgu.
mut daje prawo zapisu nazwie, a nie wartości — to rozróżnienie, na którym stoi cała strona. Ten sam łańcuch znaków (String) potrafi być niemutowalny pod jedną nazwą i zapisywalny pod następną (let owned = String::from("Ada"); a potem let mut moved = owned;), choć bajty na stercie są dokładnie te same. Obietnica jest przy tym pilnowana w obie strony: mut, do którego nikt nigdy nie pisze, to ostrzeżenie unused_mut — variable does not need to be mutable. Dlatego nie dopisuj mut „na wszelki wypadek”, jak w językach, gdzie nic to nie kosztuje. Zysk jest w czytaniu cudzego kodu: skoro mut zawsze znaczy tu coś się zmienia, wystarczy przejrzeć funkcję pod kątem tego jednego słowa, żeby wiedzieć, które nazwy są ruchome. Dla przychodzącego z ABAP-a to odruch odwrotny do wyuczonego — tam wszystko z DATA jest zapisywalne na zawsze i szuka się CONSTANTS; tutaj szuka się tych trzech mut.
Ostatnia pułapka jest składniowa i po polsku szczególnie łatwo w nią wpaść, bo oba zapisy nazwalibyśmy „przypisaniem”. let spaces = " "; a potem let spaces = spaces.len(); kompiluje się bez mut i bez błędu — bo nic nie zostało przypisane. Powstała druga zmienna, która przejęła nazwę, i wolno jej mieć inny typ (tutaj &str staje się usize o wartości 3). To jest przesłanianie (shadowing), zupełnie inny mechanizm niż mut, a całą różnicę widać w dwóch pytaniach z tabeli powyżej: x = 20; wymaga mut i typu zmienić nie może, a let x = 20; nie wymaga niczego i typ zmienić może.
Szukaj po polsku: zmienne mutowalne w Ruscie · przesłanianie zmiennych · wiązanie nazwy z wartością · rust E0384 cannot assign twice · rust let vs const · rust unused_mut warning