Rust · Beginner
Variables and Mutability in Rust
In short: A Rust variable is created with let and cannot be reassigned unless it is declared let mut. A second let with the same name shadows the first and may change its type, and const declares a compile-time value that never changes.
Immutable by default, and why
In Rust, let count = 3; creates a binding that can never be changed. Trying to write count = 4; later is a compile-time error, not a warning. To allow reassignment you say so at the declaration: let mut count = 3;. This is the reverse of most languages, where everything is mutable unless you add a keyword such as final, and the reversal is deliberate. When you read Rust code, every mut marks a value that may change over time, so anything without it can be relied on to stay as it was. The compiler relies on the same promise: a value that never changes can be shared freely, which becomes central in the ownership lesson.
Declaring a variable mut does not change its type; it changes what you are allowed to do with the binding. count += 1 needs mut; so does calling a method that modifies the value in place, such as push_str on a String. If you add mut and never mutate, the compiler warns about an unused mut, which is a hint to remove it.
The compiler infers the type of most variables from the value, so let count = 3; is an i32 and let price = 3.5; is an f64. Add an annotation when the value does not settle the type, or when you want a different one: let seats: u32 = 40;. You can also declare a variable without a value, let total;, and assign it exactly once later; using it before that assignment is an error, so there is no such thing as an uninitialised variable in Rust.
Rust lets you declare a new variable with the same name as an existing one. This is called shadowing. The second let creates a fresh binding that hides the first for the rest of its scope; it is not an assignment, so it works on immutable variables and the new variable may even have a different type. Shadowing is the idiomatic way to turn the text read from input into a number: let line: u32 = line.trim().parse().unwrap(); reuses the good name instead of inventing line_text and line_number. A shadow declared inside a block disappears at the block's closing brace, and the outer variable is visible again.
A const is different from an immutable variable. Its value must be fixed at compile time, it must carry a type annotation, its name is written in upper snake case, and it can be declared outside any function so that every function can use it. The compiler substitutes the value at each use. A static also lives for the whole program but occupies one fixed memory location; you rarely need one, and a mutable static requires unsafe, so prefer const for fixed numbers and text.
Syntax
let capacity = 40; // immutable, type inferred (i32)
let mut aboard = 0; // mutable
aboard += 12; // allowed because of mut
let seats: u32 = 40; // explicit type
let label; // declared now...
label = "ferry"; // ...assigned exactly once later
let line = " 7 ";
let line = line.trim(); // shadowing: a new variable, same name
let line: u32 = line.parse().unwrap(); // shadowing again, with a new type
const MAX_CRATES: u32 = 12; // compile-time constant; the type is requiredlet introduces a variable, mut allows it to be reassigned or modified in place, and repeating let with an existing name shadows the earlier variable rather than assigning to it.
A counter that changes and a limit that does not
Only aboard is declared mut, because only aboard is modified after its declaration.
fn main() {
let capacity = 40;
let mut aboard = 0;
println!("Boarding started, {} aboard", aboard);
aboard += 12;
aboard += 9;
println!("After two groups: {} aboard", aboard);
let free = capacity - aboard;
println!("Seats still free: {}", free);
}Output
Boarding started, 0 aboard After two groups: 21 aboard Seats still free: 19
capacity never changes, so it is declared without mut; if a later line tried to assign to it, the program would not compile. aboard is declared mut because the two += lines modify it. free is computed once from the other two values and is immutable as well. Reading the declarations tells you, before you read the rest of the function, which values can move.
Shadowing to turn input text into a number
The program is run with the input line 7 (with spaces around the digit).
use std::io::BufRead;
fn main() {
let mut lines = std::io::stdin().lock().lines();
let crates = lines.next().unwrap().unwrap();
println!("Read the text {:?}", crates);
let crates: u32 = crates.trim().parse().unwrap();
let crates = crates * 24;
println!("Bottles: {}", crates);
}Input given to the program: 7
Output
Read the text " 7 " Bottles: 168
The first crates holds the raw text from standard input, printed with {:?} so the surrounding spaces are visible inside quotes. The second let crates shadows it with a u32 parsed from the trimmed text; the type annotation tells parse() what to produce. The third let crates shadows again with the multiplied value. None of these lines is an assignment, so none needs mut, and at the end there is one live variable named crates with the type you want. This is the standard pattern for reading numbers in every exercise.
Constants and block scope
Watch what the last line prints after the inner block has shadowed crates.
const CRATES_PER_PALLET: u32 = 12;
const DEPOT: &str = "North Quay";
fn main() {
let pallets = 3;
let crates = pallets * CRATES_PER_PALLET;
println!("{}: {} crates", DEPOT, crates);
{
let crates = crates + 5;
println!("Inside the block: {} crates", crates);
}
println!("After the block: {} crates", crates);
}Output
North Quay: 36 crates Inside the block: 41 crates After the block: 36 crates
CRATES_PER_PALLET and DEPOT are constants: declared outside main with explicit types, usable anywhere in the file, fixed at compile time. Inside the inner block, let crates = crates + 5; creates a second crates that shadows the first; the right-hand side still reads the outer one. When the block ends, that shadow is gone and the final line prints the outer value again. Shadowing never modifies the original binding.
Common mistakes
Assigning to a variable declared without mut
Why it goes wrong: The compiler reports error E0384, cannot assign twice to immutable variable.
letalone is a promise that the value will not change, and the compiler enforces it.Fix: Add
mutat the declaration if the value genuinely changes, or compute the new value into a new variable.Rust · fixlet mut total = 0; total += 5;Writing a const without a type
Why it goes wrong:
const LIMIT = 12;is rejected with missing type forconstitem. Unlikelet, a constant's type is never inferred, because a constant can be used from anywhere in the file and the compiler wants its meaning fixed at the declaration.Fix: Write the type:
const LIMIT: u32 = 12;.Expecting a shadow inside a block to change the outer variable
Why it goes wrong: A
letinside braces creates a new variable that exists only until the closing brace. The outer variable is untouched, so code after the block sees the old value.Fix: To update the outer value, declare it
mutand assign with=or+=inside the block instead of writinglet.Rust · fixlet mut crates = 36; { crates += 5; } println!("{}", crates); // 41Using a variable outside the block where it was declared
Why it goes wrong: Variables live until the end of the block that declares them. Referring to one after that block gives error E0425, cannot find value in this scope, and the compiler notes that the binding exists in a different scope.
Fix: Declare the variable before the block, at the level where it is needed, and assign to it inside.
let, let mut, shadowing and const
| Form | Can be reassigned? | Can change type? | When the value is fixed |
|---|---|---|---|
let x = 5; | no | no | at run time, once |
let mut x = 5; | yes | no | changes while the program runs |
let x = ...; let x = ...; | each let is a new variable | yes | at each declaration |
const X: u32 = 5; | no | no | at compile time |
Where you use this
Reading configuration-like values at the top of a program is where the three forms meet. A tool that packs bottles into crates declares const BOTTLES_PER_CRATE: u32 = 24; because that number is a fact about the crates, not something the program computes. It reads the number of bottles from input into a text variable, then shadows it with the parsed u32, so the rest of the code never sees the raw text. The running total of crates packed is the only value that changes, so it is the only let mut. When a colleague opens the file, that single mut tells them exactly where the state of the program lives.
const BOTTLES_PER_CRATE: u32 = 24;
let bottles = line.trim().parse::<u32>().unwrap();
let mut crates_packed = 0;Key points
letcreates an immutable binding; reassigning it is a compile-time error (E0384).let mutallows reassignment and in-place modification; the compiler warns ifmutis unused.- Types are inferred from the value; add
: Typewhen the value does not decide it. - A second
letwith the same name shadows the first and may change the type; it is not an assignment. - A shadow declared in a block ends with the block.
const NAME: Type = value;is fixed at compile time, needs its type, and is written in upper snake case.
Try it yourself
The program prints the number of pallets. Below that line, add a mutable total that starts at 0, add pallets * CRATES_PER_PALLET to it, then add 5 loose crates, and print Total crates: followed by the value.
const CRATES_PER_PALLET: u32 = 12;
fn main() {
let pallets = 3;
println!("Pallets: {}", pallets);
}
Pallets: 3
Total crates: 41const CRATES_PER_PALLET: u32 = 12;
fn main() {
let pallets = 3;
println!("Pallets: {}", pallets);
let mut total = 0;
total += pallets * CRATES_PER_PALLET;
total += 5;
println!("Total crates: {}", total);
}
Practise this
Related lessons
- Rust Syntax and Your First ProgramHow a Rust program is put together: fn main, statements and semicolons, println! formatting, comments, blocks, and reading one line of input.8 min
- Data Types in RustRust's scalar and compound types: integer and float sizes, bool, char, tuples and arrays, plus casting with as and parsing numbers from input text.9 min
Frequently asked questions
Is a Rust variable declared with let a constant?
No. An immutable let binding gets its value at run time, for example from input, and simply cannot be reassigned afterwards; a const must be computable by the compiler and is substituted at each use. Both never change, but only const can be declared outside a function and used in places that need a compile-time value, such as an array length.
Why does Rust allow shadowing when it forbids reassignment?
Because they are different operations. Reassignment changes the value behind an existing name, which the compiler must be able to track for safety. Shadowing creates a new variable and merely reuses the name, so nothing that already referred to the old variable is affected. It lets you transform a value through a few steps, such as trimming and parsing input, without inventing a new name for each step.
Does mut change where or how a variable is stored?
No. mut is a property of the binding, not of the value or its storage. A let mut integer and a let integer occupy the same kind of memory; the difference is entirely in what the compiler lets you do with the name. This is why moving a value into a new let mut binding makes it mutable again.
How this page was checked. Every program on it was run with rustc 1.94 at build time by the publishing checks, and the output shown is what it printed. Running Rust inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with rustc 1.94 locally.