Variables & Mutability 📦
Programs need to remember things: scores, names, totals. Variables are how. And Rust has a strong opinion about them that will surprise you.
Variables are labeled boxes
Picture a box with a label on it. let creates the box, the label
goes on the left of =, and what's inside goes on the right:
fn main() {
let apples = 5;
let name = "Rusty";
println!("{name} has {apples} apples");
}
Inside the quotes of println!, curly braces { } are
little windows: {name} means "show the value of name here."
The plot twist: boxes are sealed 🔒
In most languages, you can change a variable whenever you like. In Rust, this does not compile:
fn main() {
let apples = 5;
apples = 6; // ❌ nope!
}
error[E0384]: cannot assign twice to immutable variable `apples`
--> src/main.rs:3:5
|
2 | let apples = 5;
| ------ first assignment to `apples`
3 | apples = 6;
| ^^^^^^^^^^ cannot assign twice to immutable variable
|
help: consider making this binding mutable: `mut apples`
Rust variables are immutable by default: sealed boxes. Why? Because a huge share of bugs come from data changing when you didn't expect it. If nothing can change unless you explicitly allow it, whole categories of bugs vanish. Safety by default is the Rust theme, and this is your first taste of it.
Opening the lid: mut
When you genuinely want a value to change, say so out loud with mut:
fn main() {
let mut score = 0;
score = score + 10; // ✅ allowed, we asked for a mutable box
score += 5; // shorthand for score = score + 5
println!("score: {score}");
}
Now anyone reading your code (including you, at 2am) knows exactly which values can change and which are set in stone. It's documentation the compiler enforces.
Constants: carved in rock 🪨
For values that will never, ever change, use const. By
convention the name is ALL_CAPS, and you must write the type (more on types next lesson):
const MAX_PLAYERS: u32 = 8;
const SPEED_OF_LIGHT_MS: u32 = 299_792_458; // underscores make big numbers readable
fn main() {
println!("Up to {MAX_PLAYERS} players allowed.");
}
Shadowing: the box-swap trick 🎩
You can reuse a name with a fresh let. That's not mutation;
it's a brand-new box wearing the old label:
fn main() {
let message = " hello ";
let message = message.trim(); // new box: spaces removed
let message = message.len(); // new box: now it's a NUMBER (5)
println!("{message}");
}
Notice the superpower: shadowing may even change the type. Text became
a number. Rustaceans use this for step-by-step transformations, so you don't need
names like message_trimmed_final_v2.
- Reassigning without
mut: read the error; it literally suggests the fix. - Adding
mutto everything "just in case": resist! Default to immutable; addmutonly when the compiler asks and it makes sense. - Declaring a variable you never use: Rust warns you (prefix with
_like_unusedto hush it on purpose).
Fix the crab counter
This program won't compile. Fix it two different ways: once with
mut, once with shadowing.
fn main() {
let crabs = 10;
crabs = crabs + 5;
println!("{crabs} crabs on the beach");
}
Reveal solution
// Way 1: mutable box
fn main() {
let mut crabs = 10;
crabs = crabs + 5;
println!("{crabs} crabs on the beach");
}
// Way 2: shadowing (a new box each time)
fn main() {
let crabs = 10;
let crabs = crabs + 5;
println!("{crabs} crabs on the beach");
}
Level-up counter
Create a mutable level starting at 1. Add 1 to it three times,
printing "Level up! Now level N" after each change.
Reveal solution
fn main() {
let mut level = 1;
level += 1;
println!("Level up! Now level {level}");
level += 1;
println!("Level up! Now level {level}");
level += 1;
println!("Level up! Now level {level}");
}
Typing the same thing three times felt silly, right? Good instinct: loops (Lesson 5) will fix that.
Predict the output
Without running it, what does this print?
fn main() {
let x = 2;
let x = x * x;
let x = x + 1;
println!("{x}");
}
Reveal answer
5. Each let makes a new x from the old
one: 2 → 4 → 5. Shadowing in action.