Rust 4 🦀 Variables, Immutability, and the mut Keyword
Variables in Rust are immutable by default. This single design decision surprises nearly every programmer coming from another language, where mutation is the norm and constancy is the exception. In Rust the default is reversed: you must explicitly opt in to mutability using the mut keyword. The language makes you declare your intent to change a value, and the compiler holds you to that declaration.
This is not a stylistic preference. Immutability by default eliminates entire categories of bugs — accidental reassignments, unexpected side effects from shared state, and data races in concurrent code. When a value cannot change, reasoning about code becomes dramatically simpler because you only need to understand what a value is, not what it might become. The compiler enforces this at every point in the program.
This chapter covers how to declare variables, what immutability means in practice, when and how to use mut, the distinction between variables and constants, and the related concept of shadowing. We will also look at type inference and how Rust balances explicitness with ergonomics.
Key point: Rust variables are immutable by default, and you must use the mut keyword to make them changeable. This inverts the convention of most languages and forces you to declare mutation intent explicitly.
Why immutability by default exists
The accidental mutation problem. In most languages, any variable can be changed by any code that has access to it. A function might modify a global, a loop counter might be reassigned in a nested scope, or a shared reference might be altered by another thread. These accidental mutations are a leading source of bugs because they violate the reader’s assumptions. Rust’s default prevents this: if you see a variable without mut, you know nothing can change it between its declaration and its last use.
The concurrency problem. Data races occur when two threads access the same memory concurrently and at least one of them writes. Languages without compile-time immutability guarantees must rely on runtime locks, which are easy to forget and hard to verify. In Rust, an immutable variable can be shared freely across threads because no thread can modify it. The compiler knows this and permits the sharing without synchronization. This is a direct consequence of immutability being the default.
The reasoning problem. When you read code, you build a mental model of what each variable contains at each point. If variables can change at any time, you must trace every assignment and every function call to know the current value. If they cannot change, you read the declaration once and that is the value, forever. This makes code review, debugging, and refactoring dramatically easier, especially in large codebases where the declaration and the use may be hundreds of lines apart.
The optimization problem. Compilers can optimize immutable values aggressively. A constant can be inlined at every use site, a loop invariant can be hoisted, and a value that never changes can be cached in a register rather than reloaded from memory. When mutation is possible, the compiler must be conservative because it cannot assume a value stays the same across a function call or an intervening statement. Immutability gives the optimizer freedom it otherwise lacks.
The intent problem. When you see mut on a variable, you know immediately that this value changes somewhere in the function. The keyword is a signal to the reader: pay attention, this is not constant. Conversely, the absence of mut is a guarantee. By requiring an explicit keyword for mutation, Rust makes the common case (no mutation) quiet and the exceptional case (mutation) visible. Over time, this shapes code toward purity because adding mut feels like a decision worth justifying.
a. Declaring variables and using mut
Variables are declared with the let keyword. Without mut, the variable cannot be reassigned after its initial binding:
let x = 5;
println!("{}", x); // prints 5
// x = 6; // error: cannot assign twice to immutable variable
Attempting to reassign x produces a compile error, not a runtime error. The compiler catches the mistake before the program ever runs. This is the essence of Rust’s approach: mistakes are found at compile time, not in production.
To allow reassignment, add mut after let:
let mut x = 5;
println!("{}", x); // prints 5
x = 6;
println!("{}", x); // prints 6
The mut keyword appears in the binding, not in the assignment. It is part of the variable’s type signature in the broadest sense — it tells the compiler and the reader that this binding is changeable. You use mut exactly once, at declaration, and can then assign to the variable as many times as you like.
The mut keyword also applies to references. A mutable reference, written &mut T, allows the holder to modify the referred-to value. An immutable reference, written &T, does not. We will cover references in depth in a later chapter, but the keyword’s role is consistent: it marks places where mutation is permitted.
b. Type inference and annotation
Rust is statically typed, but it usually infers types for you. In most cases, you do not need to write the type of a variable because the compiler can determine it from the initial value:
let x = 5; // inferred as i32 (default integer)
let y = 3.14; // inferred as f64 (default float)
let z = "hello"; // inferred as &str
let flag = true; // inferred as bool
When inference is insufficient, you annotate the type explicitly using a colon after the variable name:
let x: u8 = 5;
let y: f32 = 3.14;
let name: String = String::from("Rust");
The default integer type is i32 and the default float type is f64. These defaults apply only when the compiler has no other information to constrain the choice. If you later use x in a context that requires a u64, the compiler will infer u64 instead, as long as the literal fits.
Type annotations are required in some cases: when the value comes from a source whose type is not self-evident, when you want a specific numeric width, or when the initial value does not determine the type. In function signatures, types are always required — inference does not cross function boundaries. This is a deliberate design choice that keeps function interfaces explicit and readable.
c. Constants, statics, and shadowing
Rust has two constructs for values that never change: const and static. Both are declared with uppercase names by convention, and both require explicit type annotations. They differ in their storage and usage.
A const is a compile-time constant that is inlined at every use site. It must be initialized with a constant expression, meaning the value must be computable at compile time:
const MAX_POINTS: u32 = 100_000;
const SECONDS_PER_DAY: u32 = 24 * 60 * 60;
Constants can be declared in any scope, including inside functions, and they exist for the entire life of the program. Because they are inlined, they have no fixed memory address in the usual sense — each use site gets its own copy of the value.
A static is a value with a fixed memory address for the entire life of the program. Statics can be mutable, but mutable statics are unsafe to access because Rust cannot guarantee data-race freedom for them:
static LANGUAGE: &str = "Rust";
static mut COUNTER: u32 = 0;
Mutating a static mut requires an unsafe block, and in practice, most programs use safer alternatives like AtomicUsize or Mutex when they need global mutable state. The recommendation is to avoid static mut entirely unless you have a clear reason and understand the safety implications.
Shadowing is a separate mechanism that allows you to declare a new variable with the same name as an existing one. The new variable shadows the old one for the rest of the scope:
let x = 5;
let x = x + 1; // new x = 6, shadows old x
let x = x * 2; // new x = 12, shadows previous x
println!("{}", x); // prints 12
Shadowing is not mutation. The original x is not changed; it is hidden by a new binding. This distinction matters because shadowing can change the type of the variable:
let spaces = " "; // &str
let spaces = spaces.len(); // usize — different type, same name
This pattern is common when transforming data: a string becomes a number, a number becomes a formatted string, and each transformation gets a name appropriate to its state. Without shadowing, you would need distinct names like spaces_str and spaces_len, cluttering the namespace. With shadowing, you reuse the name and let the type change.
Complete Example Session
// ============================================
// PART 1: IMMUTABLE BY DEFAULT
// ============================================
// A let binding without mut cannot be reassigned.
fn main() {
let x = 5;
println!("x is {}", x);
// x = 6; // error[E0384]: cannot assign twice
}
// ============================================
// PART 2: MAKING A VARIABLE MUTABLE
// ============================================
// Add mut to permit reassignment.
fn main() {
let mut x = 5;
println!("x is {}", x); // 5
x = 6;
println!("x is {}", x); // 6
}
// ============================================
// PART 3: TYPE INFERENCE
// ============================================
// The compiler infers types from usage.
fn main() {
let a = 5; // i32 by default
let b = 3.14; // f64 by default
let c = "hello"; // &str
let d = true; // bool
println!("{} {} {} {}", a, b, c, d);
}
// ============================================
// PART 4: EXPLICIT TYPE ANNOTATIONS
// ============================================
// When inference is not enough, annotate.
fn main() {
let x: u8 = 255;
let y: f32 = 1.5;
let z: char = 'R';
println!("{} {} {}", x, y, z);
}
// ============================================
// PART 5: CONSTANTS
// ============================================
// const values are compile-time inlined.
const MAX_USERS: u32 = 10_000;
const SECONDS_PER_HOUR: u32 = 60 * 60;
fn main() {
println!("Max users: {}", MAX_USERS);
println!("Seconds per hour: {}", SECONDS_PER_HOUR);
}
// ============================================
// PART 6: STATICS
// ============================================
// static values have a fixed memory address.
static LANGUAGE: &str = "Rust";
fn main() {
println!("Language: {}", LANGUAGE);
}
// ============================================
// PART 7: SHADOWING
// ============================================
// A new let hides the previous binding.
fn main() {
let x = 5;
let x = x + 1;
let x = x * 2;
println!("x is {}", x); // 12
}
// ============================================
// PART 8: SHADOWING CAN CHANGE TYPE
// ============================================
// The shadowed name can hold a different type.
fn main() {
let spaces = " ";
let spaces = spaces.len();
println!("spaces is {}", spaces); // 3
}
// ============================================
// PART 9: MUTABLE VARIABLE WITH TYPE CHANGE
// ============================================
// mut does NOT allow type change. This is
// a key difference from shadowing.
fn main() {
let mut value = 10;
value = 20;
// value = "hello"; // error: mismatched types
println!("{}", value);
}
// ============================================
// PART 10: PRACTICAL PATTERN
// ============================================
// Immutability by default, mut when needed.
fn main() {
let base = 100; // never changes
let mut total = 0; // accumulates
for i in 1..=5 {
total += base * i;
}
println!("Total: {}", total); // 1500
}
These ten parts illustrate the core rules: immutability is the default, mut enables reassignment, types are inferred or annotated, constants and statics hold unchanging values, shadowing rebinds names, and mut does not permit changing a variable’s type. The final example shows the typical pattern: a mix of immutable and mutable bindings, each used according to whether the value changes.
Quick Reference
Binding Forms
| Form | Mutable | Type Change | Scope |
|---|---|---|---|
let x = 5; | No | No | Current block |
let mut x = 5; | Yes | No | Current block |
let x = 5; let x = "s"; | No | Yes (shadowing) | Current block |
const X: u32 = 5; | No | N/A | Any scope |
static X: &str = "s"; | No | N/A | Program lifetime |
Default Types
| Literal | Default Type |
|---|---|
| Integer | i32 |
| Float | f64 |
| String literal | &str |
| Boolean | bool |
| Character | char |
When to Use What
| Situation | Choice |
|---|---|
| Value never changes | let |
| Value changes in place | let mut |
| Value transformed, type changes | Shadowing |
| Compile-time constant | const |
| Global with fixed address | static |
Best Practices
✅ Do This:
let x = 5; // Immutable by default
let mut count = 0; // mut only when reassigning
const MAX: u32 = 100; // Uppercase for constants
let x = x + 1; // Shadow to transform value
let spaces = " "; // Shadowing to change type
let spaces = spaces.len();
❌ Don’t Do This:
let mut x = 5; // ❌ mut when never reassigned
const max: u32 = 100; // ❌ Lowercase constant name
let mut spaces = " "; // ❌ Cannot change type with mut
static mut COUNTER: u32 = 0; // ❌ Avoid static mut
let x: u32 = 5; // ❌ Annotation when inferable
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
cannot assign twice to immutable variable | Missing mut on the binding | Add mut after let |
mismatched types on reassignment | mut does not allow type change | Use shadowing instead |
unused mut warning | Declared mut but never reassigned | Remove mut |
| Constant not inlined | Using static when const suffices | Prefer const for compile-time values |
static mut unsafe error | Accessing mutable static without unsafe | Use AtomicUsize, Mutex, or OnceLock |
| Shadowing confusion | Reusing a name across many lines | Keep shadowing blocks short |
Real-World Examples
1. Loop Counter
let mut count = 0;
for _ in 0..10 {
count += 1;
}
2. Accumulator
let mut total = 0.0;
for price in prices {
total += price;
}
3. Constant Configuration
const TIMEOUT_SECONDS: u64 = 30;
const MAX_RETRIES: u8 = 3;
4. Global String
static APP_NAME: &str = "MyApp";
5. Shadowing Through Transformation
let input = "42";
let input: i32 = input.parse().unwrap();
let input = input * 2;
6. Mutable Struct Field Access
let mut user = User::new("Alice");
user.age = 30;
7. Mutable Vector
let mut numbers = Vec::new();
numbers.push(1);
numbers.push(2);
8. Immutable Function Parameter
fn describe(name: &str) -> String {
format!("Hello, {}", name)
}
9. Constant Inside Function
fn calculate() -> u32 {
const MULTIPLIER: u32 = 4;
25 * MULTIPLIER
}
10. OnceLock for Global State
use std::sync::OnceLock;
static CONFIG: OnceLock<Config> = OnceLock::new();
Visual
Immutable vs Mutable Bindings
┌──────────────────────────────────────────────────────────────┐
│ IMMUTABLE (DEFAULT) MUTABLE (OPT-IN) │
│ │
│ let x = 5; let mut x = 5; │
│ │
│ ┌─────────┐ ┌─────────┐ │
│ │ x = 5 │ ← cannot │ x = 5 │ ← can reassign │
│ └─────────┘ reassign └─────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────┐ │
│ │ │ x = 6 │ │
│ │ └─────────┘ │
│ │ │ │
│ │ ▼ │
│ │ ┌─────────┐ │
│ │ │ x = 7 │ │
│ │ └─────────┘ │
│ ▼ ▼ │
│ Compiler rejects Compiler permits │
│ any assignment every assignment │
└──────────────────────────────────────────────────────────────┘
Shadowing vs Mutation
┌──────────────────────────────────────────────────────────────┐
│ MUTATION: same binding, new value │
│ │
│ let mut x = 5; │
│ x = 6; ──▶ same x, same type, new value │
│ │
│ ───────────────────────────────────────── │
│ │
│ SHADOWING: new binding, old one hidden │
│ │
│ let x = 5; │
│ let x = x + 1; ──▶ new x, hides old x │
│ let x = "hi"; ──▶ new x, hides previous x (type changed!) │
│ │
│ ───────────────────────────────────────── │
│ │
│ Mutation: Shadowing: │
│ - must use mut - no mut needed │
│ - cannot change type - can change type │
│ - one binding - multiple bindings │
│ - old value gone forever - old value dropped at shadow │
└──────────────────────────────────────────────────────────────┘
let, const, and static
┌──────────────────────────────────────────────────────────────┐
│ THREE WAYS TO BIND A NAME │
│ │
│ let x = 5; │
│ ├── runtime value │
│ ├── stack-allocated (or inlined) │
│ ├── scope-limited │
│ └── type inferred │
│ │
│ const MAX: u32 = 100; │
│ ├── compile-time value │
│ ├── inlined at every use site │
│ ├── any scope (including inside functions) │
│ └── type required │
│ │
│ static NAME: &str = "Rust"; │
│ ├── fixed memory address │
│ ├── program lifetime │
│ ├── single shared instance │
│ └── type required │
│ │
│ Use let for local data, const for constants, │
│ static only when you need a fixed address. │
└──────────────────────────────────────────────────────────────┘
Type Inference Flow
┌──────────────────────────────────────────────────────────────┐
│ HOW THE COMPILER INFERS TYPES │
│ │
│ let x = 5; │
│ │ │
│ ▼ │
│ Is there a type annotation? ─── Yes ──▶ Use it │
│ │ │
│ No │
│ │ │
│ ▼ │
│ Is there a default for this literal? │
│ │ │
│ ├── Integer ──▶ i32 │
│ ├── Float ──▶ f64 │
│ └── Other ──▶ use literal's natural type │
│ │ │
│ ▼ │
│ Later usage constrains further? │
│ │ │
│ ├── x used as u64 ──▶ retype to u64 │
│ └── x used generically ──▶ keep i32 │
│ │
│ If inference fails: "type annotations needed" │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Default binding | Immutable; cannot be reassigned |
| Mutable binding | let mut x = ... permits reassignment |
| Type inference | Compiler determines type from usage |
| Default integer | i32 |
| Default float | f64 |
| Explicit annotation | let x: u8 = 5; |
| Constant | const NAME: T = ...; compile-time inlined |
| Static | static NAME: T = ...; fixed memory address |
| Shadowing | let x = ...; let x = ...; rebinds, can change type |
| Mutation vs shadowing | Mutation reuses binding; shadowing creates new one |
Key takeaways:
- Immutability is the default in Rust. A
letbinding withoutmutcannot be reassigned, and the compiler enforces this. mutis an opt-in declaration of intent. It tells the reader and the compiler that this value changes somewhere in the function.mutdoes not allow type changes. Once a mutable variable is bound to a type, reassignments must use the same type.- Shadowing is not mutation. It creates a new binding that hides the old one, and the new binding may have a different type.
- Constants and statics differ in subtle ways.
constis compile-time inlined;statichas a fixed address and lives for the program’s duration. - Type inference is usually sufficient. Annotate only when the compiler cannot infer or when you need a specific numeric width.
- Avoid
static mut. UseAtomicUsize,Mutex, orOnceLockfor global mutable state. - Default types are
i32andf64. They apply when no other information constrains the choice.
Remember: Rust reverses the convention of most languages by making immutability the default. This is not a limitation but a design choice that pays dividends in correctness, readability, and concurrency safety. When you need to change a value, mut declares that need explicitly, and the compiler uses that information to check that mutation is legal. Shadowing offers a separate mechanism for transforming values across types, and constants and statics cover the cases where values are fixed for the program’s lifetime. The discipline of choosing between these forms — immutable, mutable, shadowed, constant, or static — shapes code that is easier to reason about and harder to break.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!