RustVariables & Mutability

Variables & Mutability in Rust

Rust takes an opinionated stance on variables: they are immutable by default. This is not a limitation — it is one of Rust's most powerful safety features. When a value cannot change, the compiler can reason about your code more precisely, eliminate entire classes of bugs, and even generate better-optimised machine code.

Creating Variables with let

Every variable in Rust is introduced with the let keyword. Without any further annotation the binding is immutable — you get one chance to assign a value and the compiler will reject any later attempt to overwrite it.

RUST
fn main() {
    let x = 5;
    println!("x is: {}", x);
}
x is: 5
What Happens When You Mutate an Immutable Variable

If you attempt to assign a new value to an immutable binding, Rust refuses to compile. Crucially, the compiler does not just say "no" — it tells you exactly what went wrong and how to fix it.

RUST
fn main() {
    let x = 5;
    x = 6; // ERROR: cannot assign twice to immutable variable
    println!("x is: {}", x);
}
error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:3:5
  |
2 |     let x = 5;
  |         -
  |         |
  |         first assignment to `x`
  |         help: consider making this binding mutable: `mut x`
3 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable
Note
The compiler message includes a concrete suggestion: add mut to the binding. Rust's error messages are designed to be actionable, not just diagnostic.
Making a Variable Mutable with mut

When you genuinely need a variable whose value will change over time, add mut after the let keyword. This makes the intent visible at the declaration site.

RUST
fn main() {
    let mut counter = 0;
    println!("counter starts at: {}", counter);

    counter = counter + 1;
    println!("counter is now: {}", counter);

    counter += 1;
    println!("counter is now: {}", counter);
}
counter starts at: 0
counter is now: 1
counter is now: 2
Tip
Use mut sparingly and purposefully. If a variable never changes after its initial assignment, leave off mut — it is free documentation that says "this value is stable."
Type Inference

Rust has a powerful type inference engine. In most situations you do not need to write the type at all — the compiler deduces it from the value you assign or how the variable is later used.

RUST
fn main() {
    let age = 30;          // inferred as i32
    let price = 9.99;      // inferred as f64
    let active = true;     // inferred as bool
    let initial = 'R';     // inferred as char

    println!("{} {} {} {}", age, price, active, initial);
}
30 9.99 true R
Explicit Type Annotations

You can — and sometimes must — write the type explicitly using a colon after the variable name. Annotations are required when the compiler cannot infer the type, when you want a different type than the default (e.g. u8 instead of i32), or simply when clarity matters more than brevity.

RUST
fn main() {
    let age: u8 = 30;
    let temperature: f32 = 36.6;
    let score: i64 = 1_000_000;
    let letter: char = 'Z';
    let flag: bool = false;

    println!("age={} temp={} score={} letter={} flag={}",
             age, temperature, score, letter, flag);
}
age=30 temp=36.6 score=1000000 letter=Z flag=false
Note
Rust allows underscores inside numeric literals as visual separators.1_000_000 and 1000000 are identical to the compiler.
Immutability vs const

Rust has two different concepts that beginners often conflate: immutable let bindings and const declarations. They serve different purposes.

Feature

let (immutable)

const

Keyword

let

const

Type annotation

Optional (inferred)

Required

Computed at

Runtime

Compile time

Allowed value

Any expression

Constant expressions only

Scope

Block / function

Any scope including global

Naming convention

snake_case

SCREAMING_SNAKE_CASE

Can be mut

Yes (let mut)

No — always immutable

RUST
const MAX_CONNECTIONS: u32 = 100;
const APP_NAME: &str = "LetCodes";

fn main() {
    let user_count = get_user_count();
    println!("{} has {} users (max {})", APP_NAME, user_count, MAX_CONNECTIONS);
}

fn get_user_count() -> u32 {
    42
}
LetCodes has 42 users (max 100)
Underscore Prefix for Unused Variables

Rust warns you when you declare a variable and never use it — this catches typos and forgotten logic. When a variable is intentionally unused, prefix its name with an underscore to silence the warning.

RUST
fn main() {
    let _placeholder = "I know I don't use this yet";
    let result = compute();
    println!("result: {}", result);
}

fn compute() -> i32 {
    let _intermediate = 99; // silences the unused-variable warning
    42
}
Tip
A single underscore _ on its own is a special "discard" pattern — it does not even bind the value. _name binds but suppresses the warning; _ discards entirely.
Variable Shadowing

Rust allows you to declare a new variable with the same name as an existing one. The new binding shadows the old one — from that point on the name refers to the new value. Shadowing is different from mutation: each let creates a brand-new binding that can even have a different type.

RUST
fn main() {
    let x = 5;
    println!("x = {}", x); // 5

    let x = x + 1;         // shadows the previous x
    println!("x = {}", x); // 6

    let x = x * 2;         // shadows again
    println!("x = {}", x); // 12

    // Shadowing can change the type entirely
    let value = "   hello   ";
    let value = value.trim();  // still &str but trimmed
    let value = value.len();   // now usize
    println!("trimmed length: {}", value);
}
x = 5
x = 6
x = 12
trimmed length: 5
Note
Shadowing is commonly used when transforming a value through several steps. Keeping the same name conveys "same thing, just refined" and avoids names like trimmed_value or value_len.
Destructuring Multiple Bindings

Rust supports destructuring in let statements, binding several names at once from a tuple or struct.

RUST
fn main() {
    let (x, y, z) = (1, 2, 3);
    println!("x={} y={} z={}", x, y, z);

    let (name, age): (&str, u8) = ("Alice", 28);
    println!("{} is {} years old", name, age);

    // Ignore parts you do not need with _
    let (first, _, third) = ("a", "b", "c");
    println!("first={} third={}", first, third);
}
x=1 y=2 z=3
Alice is 28 years old
first=a third=c
Why Immutability by Default is a Feature
  • Prevents accidental mutation — mutations must be declared at the binding site, making bugs from unexpected state changes impossible

  • Enables fearless concurrency — immutable data can be shared across threads without locks

  • Self-documenting code — the absence of mut tells the reader this value never changes

  • Better optimisations — the compiler makes stronger assumptions about immutable data

  • Compiler as collaborator — when you forget mut, the error message tells you exactly how to fix it

Success
Rust's approach flips the usual default: instead of "mutable unless marked otherwise," everything is immutable unless you explicitly opt in. This catches an entire category of bugs at compile time, before your program ever runs.
Common Patterns at a Glance

RUST
fn main() {
    // Immutable binding — the most common case
    let greeting = "Hello, Rust!";

    // Mutable counter
    let mut count = 0;
    count += 1;

    // Explicit type when the default is wrong
    let small: u8 = 255;

    // Destructuring
    let (a, b) = (10, 20);

    // Intentionally unused during development
    let _todo = "implement later";

    // Shadowing for transformation
    let raw = "  42  ";
    let raw = raw.trim();
    let number: i32 = raw.parse().expect("not a number");

    println!("{}", greeting);
    println!("count={} small={} a={} b={} number={}",
             count, small, a, b, number);
}
Hello, Rust!
count=1 small=255 a=10 b=20 number=42