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

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)

Pro Tips

📋 Quick Reference — Variables

GoalRust Syntax
Immutable variablelet x = 5;
Mutable variablelet mut x = 5;
Reassign (needs mut)x = 6;
Constantconst MAX: u32 = 100;
Shadowinglet x = x + 1;

🎉 Lesson Complete!

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

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.