Variables & Mutability
Reviewed & published by Brayan K
Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and one of its core safety choices is that variables are immutable by default.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll learn how to declare variables with let, opt in to change with mut, define compile-time constants, and reuse names through shadowing.
What You'll Learn in This Lesson
- How to declare variables with let
- Why variables are immutable by default
- How to allow changes with the mut keyword
- How to declare compile-time constants with const
- What shadowing is and how it differs from mutation
- How shadowing can change a variable's type
1️⃣ Immutable by Default, Mutable on Request
When you write let x = 5;, Rust binds the value 5 to the name x and locks it: trying to assign a new value later is a compile error. This is the opposite of most languages, and it's deliberate — it stops values from changing behind your back. When a value genuinely needs to change, you add mut to say so out loud.
fn main() {
// Variables are IMMUTABLE by default. Once set, the value cannot change.
let x = 5;
println!("x is {}", x); // -> x is 5
// The next line would FAIL to compile:
// x = 6; // error[E0384]: cannot assign twice to immutable variable
//
// To allow changes, add the "mut" keyword when you declare it.
let mut y = 5;
println!("y starts at {}", y); // -> y starts at 5
y = 6; // allowed, because y is mutable
println!("y is now {}", y); // -> y is now 6
}The key idea: immutability is the safe default, and mut is how you opt out of it for a specific variable. Anyone reading your code can see at a glance which variables are allowed to change.
2️⃣ Constants and Shadowing
A constant is a value that is fixed for the whole program and known at compile time. Unlike a normal let binding, it always needs a type annotation and can never be made mutable. Shadowing is a separate idea: you can use let again with the same name to create a new variable, even one with a different type. Read the comments closely.
fn main() {
// CONSTANTS use "const", are ALWAYS immutable, need a type, and are
// written in SCREAMING_SNAKE_CASE by convention.
const MAX_POINTS: u32 = 100_000; // underscores are just for readability
println!("Max points: {}", MAX_POINTS);
// SHADOWING: you can re-declare a name with "let". This creates a NEW
// variable that reuses the name — it is not the same as mutation.
let spaces = " "; // spaces is a string (&str)
let spaces = spaces.len(); // now spaces is a number (usize) — type changed!
println!("Number of spaces: {}", spaces); // -> Number of spaces: 3
// Shadowing also lets you transform a value while keeping the name.
let score = 50;
let score = score + 25; // a new score, built from the old one
let score = score * 2;
println!("Final score: {}", score); // -> Final score: 150
}Shadowing shines when you transform a value: notice how spaces went from a string to its length (a number) while keeping the same friendly name. With mut alone you couldn't change the type like that.
Your turn. Fill in the blanks marked ___, then run it.
// 🎯 YOUR TURN — finish the program, then run it.
fn main() {
// 1) Declare a MUTABLE counter starting at 0.
let ___ counter = 0; // 👉 add the keyword that allows changes
println!("Counter starts at {}", counter);
// 2) Increase the counter to 1, then to 2 (it must be mutable!).
counter = ___; // 👉 set it to 1
counter = ___; // 👉 set it to 2
println!("Counter is now {}", counter);
// 3) Declare an immutable constant for the maximum, with its type u32.
const MAX: ___ = 10; // 👉 the unsigned 32-bit integer type
println!("Counter {} of max {}", counter, MAX);
// ✅ Expected output:
// Counter starts at 0
// Counter is now 2
// Counter 2 of max 10
}Mini-Challenge: A Temperature Tracker
Write it yourself from the outline, run it with cargo run, and check your output against the example. You'll combine mut and shadowing in one short program.
// 🎯 MINI-CHALLENGE: a tiny temperature tracker.
//
// 1. Declare a MUTABLE variable temperature set to 20.
// 2. Print "Morning: 20".
// 3. Change temperature to 25 and print "Afternoon: 25".
// 4. Use SHADOWING to make a new immutable variable summary that is a
// String describing the day (e.g. format!("High was {}", temperature)).
// 5. Print the summary.
//
// ✅ Example output:
// Morning: 20
// Afternoon: 25
// High was 25
fn main() {
// your code here
}Common Errors (and the fix)
- "cannot assign twice to immutable variable" — You tried to reassign a variable declared without mut. Add mut: let mut x = 5;.
- "missing type for `const` item" — Constants always need a type. Write const MAX: u32 = 100;, not const MAX = 100;.
- "mismatched types" after reassigning — With mut the type can't change. To change the type, use shadowing (let again) instead of plain assignment.
- Expecting mut to change the type — mut only allows new values of the same type. Shadowing is what lets you switch types under the same name.
Pro Tips
- 💡 Default to immutable: only add mut when the compiler complains that you need it. Less mutable state means fewer bugs.
- 💡 Use shadowing for conversions: turning user input text into a number is the classic case — reuse the name with a new let.
- 💡 Name constants loudly: SCREAMING_SNAKE_CASE makes constants stand out from ordinary variables.
📋 Quick Reference — Variables
| Goal | Rust Syntax |
|---|---|
| Immutable variable | let x = 5; |
| Mutable variable | let mut x = 5; |
| Reassign (needs mut) | x = 6; |
| Constant | const MAX: u32 = 100; |
| Shadowing | let x = x + 1; |
🎉 Lesson Complete!
- ✅ Variables are declared with let and are immutable by default
- ✅ Add mut to allow a variable to change
- ✅ const declares a compile-time constant and always needs a type
- ✅ Shadowing reuses a name with a new let — and can change the type
Practice quiz
Are Rust variables mutable or immutable by default?
- Mutable
- It depends on the type
- Immutable
- Always constant
Answer: Immutable. Variables are immutable by default; you opt into change with mut.
Which keyword makes a variable able to change?
- mut
- var
- let
- const
Answer: mut. Adding mut, as in let mut x = 5;, allows reassignment.
What error does reassigning an immutable variable produce?
- type mismatch
- missing semicolon
- no such variable
- cannot assign twice to immutable variable
Answer: cannot assign twice to immutable variable. Reassigning a non-mut variable gives 'cannot assign twice to immutable variable'.
What does shadowing do?
- Mutates the same variable
- Creates a new variable that reuses the name
- Deletes the variable
- Makes it global
Answer: Creates a new variable that reuses the name. Shadowing uses let again to create a brand-new variable with the same name.
Can shadowing change a variable's type?
- Yes
- No, never
- Only for numbers
- Only with mut
Answer: Yes. Because shadowing makes a new variable, it can have a different type.
Can a variable marked mut change its type on reassignment?
- Yes
- Only to a larger type
- No, the type must stay the same
- Only once
Answer: No, the type must stay the same. mut allows new values of the same type; to change type you shadow.
Which keyword declares a compile-time constant?
- let
- const
- mut
- static let
Answer: const. const declares a constant that is fixed at compile time.
Do constants require a type annotation?
- No, it is optional
- Only for integers
- Only inside functions
- Yes, always
Answer: Yes, always. Constants always need an explicit type, like const MAX: u32 = 100;.
What naming convention do constants use by convention?
- camelCase
- SCREAMING_SNAKE_CASE
- snake_case
- PascalCase
Answer: SCREAMING_SNAKE_CASE. Constants are conventionally named in SCREAMING_SNAKE_CASE.
What do underscores in a number like 100_000 do?
- Multiply the value
- Mark it as a float
- Act as visual separators with no effect
- Cause a compile error
Answer: Act as visual separators with no effect. Underscores are ignored separators; 100_000 equals 100000.
Continue this course
- Previous: Introduction to Rust
- Next: Cargo, Crates & the Module Workflow — cargo new/build/run/test, Cargo.toml, and organising code into modules
- Quick reference: Rust cheat sheet › Basics
Frequently asked questions
Why are Rust variables immutable by default?
Immutability prevents a whole category of bugs where a value changes unexpectedly somewhere far away in your code. By making you opt in to mutation with mut, Rust makes mutable state visible and intentional. It also helps the compiler reason about your program and can enable optimisations. In practice you only mark the variables that genuinely need to change, and everything else stays safely fixed.
What is the difference between mut and shadowing?
Marking a variable mut lets you reassign the same variable, but its type must stay the same. Shadowing uses let again with the same name to create a brand-new variable that happens to reuse the name — and it can have a different type. So 'let mut x = 5; x = 6;' reuses one variable, while 'let x = 5; let x = "five";' creates two different variables that share a name. Shadowing is handy for transforming a value (like parsing text into a number) without inventing a new name.
How are constants different from immutable variables?
Both can't change, but constants go further. You declare them with const instead of let, you must always annotate their type, they can be declared in any scope including the global scope, and their value must be a constant expression known at compile time (not the result of a function call made at runtime). By convention they use SCREAMING_SNAKE_CASE names, like const MAX_USERS: u32 = 100.
Why can I use underscores in numbers like 100_000?
Rust lets you place underscores anywhere inside a numeric literal purely as visual separators — the compiler ignores them. Writing 1_000_000 instead of 1000000 makes large numbers far easier to read at a glance. They have no effect on the value; 100_000 and 100000 are exactly the same number.