| |

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

FormMutableType ChangeScope
let x = 5;NoNoCurrent block
let mut x = 5;YesNoCurrent block
let x = 5; let x = "s";NoYes (shadowing)Current block
const X: u32 = 5;NoN/AAny scope
static X: &str = "s";NoN/AProgram lifetime

Default Types

LiteralDefault Type
Integeri32
Floatf64
String literal&str
Booleanbool
Characterchar

When to Use What

SituationChoice
Value never changeslet
Value changes in placelet mut
Value transformed, type changesShadowing
Compile-time constantconst
Global with fixed addressstatic

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

PitfallWhy It HappensFix
cannot assign twice to immutable variableMissing mut on the bindingAdd mut after let
mismatched types on reassignmentmut does not allow type changeUse shadowing instead
unused mut warningDeclared mut but never reassignedRemove mut
Constant not inlinedUsing static when const sufficesPrefer const for compile-time values
static mut unsafe errorAccessing mutable static without unsafeUse AtomicUsize, Mutex, or OnceLock
Shadowing confusionReusing a name across many linesKeep 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

ItemValue
Default bindingImmutable; cannot be reassigned
Mutable bindinglet mut x = ... permits reassignment
Type inferenceCompiler determines type from usage
Default integeri32
Default floatf64
Explicit annotationlet x: u8 = 5;
Constantconst NAME: T = ...; compile-time inlined
Staticstatic NAME: T = ...; fixed memory address
Shadowinglet x = ...; let x = ...; rebinds, can change type
Mutation vs shadowingMutation reuses binding; shadowing creates new one

Key takeaways:

  • Immutability is the default in Rust. A let binding without mut cannot be reassigned, and the compiler enforces this.
  • mut is an opt-in declaration of intent. It tells the reader and the compiler that this value changes somewhere in the function.
  • mut does 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. const is compile-time inlined; static has 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. Use AtomicUsize, Mutex, or OnceLock for global mutable state.
  • Default types are i32 and f64. 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!