Variables, Mutability & Shadowing

Rust · Lesson 4 of 31 7 min read Updated

A variable is a name for a value your program uses, such as a player's score or a train number. In Rust you create variables with let, and you will quickly notice something unusual: once a value is stored, you cannot change it unless you say so.

In this lesson you will learn how to declare variables, why they are immutable by default, how to make them mutable, how constants differ from variables, and a Rust feature called shadowing that lets you reuse a name.

Declaring variables with let

let binds a value to a name. Rust works out the type from the value, so you usually do not have to write it:

Rust
fn main() {
    let train = "Shatabdi Express";
    let coach = 'C';
    let seats = 78;
    let fare: f64 = 1155.0;
    println!("{train}, coach {coach}: {seats} seats, fare Rs. {fare:.2}");
}
Output
Shatabdi Express, coach C: 78 seats, fare Rs. 1155.00

Rust guessed that train is text, coach is a single character and seats is a whole number. For fare we wrote the type ourselves with : f64 (a decimal number); this is called a type annotation. Inside println!, {fare:.2} prints the value with exactly two digits after the decimal point.

Variable names use snake_case: lowercase words joined by underscores, such as total_marks. The compiler warns you if you write totalMarks.

Immutable by default

Try to change a variable after creating it:

Rust
fn main() {
    let wickets = 3;
    println!("Wickets: {wickets}");
    wickets = 4;
    println!("Wickets: {wickets}");
}
Output
error[E0384]: cannot assign twice to immutable variable `wickets`
 --> src/main.rs:4:5
  |
2 |     let wickets = 3;
  |         ------- first assignment to `wickets`
3 |     println!("Wickets: {wickets}");
4 |     wickets = 4;
  |     ^^^^^^^^^^^ cannot assign twice to immutable variable

The compiler also suggests the fix: "consider making this binding mutable". Why is immutable the default? In a big program, a value that can change anywhere is hard to reason about. When a variable cannot change, you only need to look at the line where it was created to know its value. Immutability also makes it safe to share data between threads, which you will see in Fearless Concurrency.

Making a variable mutable with mut

When a value really needs to change, add mut after let:

Rust
fn main() {
    let mut runs = 0;
    runs += 4;
    runs += 6;
    runs += 1;
    println!("Runs after three balls: {runs}");
}
Output
Runs after three balls: 11

runs += 4 is short for runs = runs + 4. The word mut is a signal to every reader: "watch this value, it changes". Use it only when needed; the compiler warns you if you write mut on a variable that never changes.

A variable can also be declared first and given its value later, as long as it is assigned exactly once before use. The compiler checks every path:

Rust
fn main() {
    let percentage = 82;
    let grade: char;
    if percentage >= 75 {
        grade = 'A';
    } else {
        grade = 'B';
    }
    println!("Grade: {grade}");
}
Output
Grade: A

Constants and statics

A constant is a value fixed when the program is compiled. Declare it with const, write its name in SCREAMING_SNAKE_CASE, and always give its type:

Rust
const MAX_STUDENTS: u32 = 60;
const GST_PERCENT: f64 = 18.0;

fn main() {
    let price = 1299.0;
    let gst = price * GST_PERCENT / 100.0;
    println!("Seats per batch: {}", MAX_STUDENTS);
    println!("GST on Rs. {price:.2} is Rs. {gst:.2}");
}
Output
Seats per batch: 60
GST on Rs. 1299.00 is Rs. 233.82
letlet mutconst
Can changeNoYesNo
Type annotationOptionalOptionalRequired
Value knownAt run timeAt run timeAt compile time
Where allowedInside functionsInside functionsAnywhere, including outside functions

Constants can be used anywhere in the file, and the compiler copies the value into each place it is used. Rust also has static items, written like static APP_NAME: &str = "Marks Report";. A static lives at one fixed memory address for the whole program; you will rarely need one, and mutable statics require unsafe code (see Unsafe Rust).

Shadowing

You can declare a new variable with the same name as an earlier one. The new variable shadows the old one: from that line on, the name refers to the new value.

Rust
fn main() {
    let marks = "86";                          // text from a form
    let marks: u32 = marks.trim().parse().unwrap();   // now a number
    let marks = marks + 5;                     // add grace marks
    println!("Final marks: {marks}");

    {
        let marks = marks * 2;
        println!("Inside the block: {marks}");
    }
    println!("After the block: {marks}");
}
Output
Final marks: 91
Inside the block: 182
After the block: 91

The first marks is text. .parse() converts it to a number, and .unwrap() takes the number out of the result (you will learn the safe way to handle failure in Option & Result). Each let creates a brand-new variable, so the type can change from text to u32. Inside the inner block the shadow lasts only until the closing }, so after the block marks is 91 again.

Shadowing is different from mut. With mut you change the value in the same variable, and the type must stay the same:

Rust
fn main() {
    let mut phone = "98765 43210";
    phone = phone.len();
}
Output
error[E0308]: mismatched types
 --> src/main.rs:3:13
  |
2 |     let mut phone = "98765 43210";
  |                     ------------- expected due to this value
3 |     phone = phone.len();
  |             ^^^^^^^^^^^ expected `&str`, found `usize`

phone holds text (type &str), and .len() returns a number (usize), so the assignment is rejected. Writing let phone_len = phone.len(); or shadowing with let phone = phone.len(); both work. Shadowing is useful for small transformations of one value; a new, clearer name is often better when the meaning changes.

Common mistakes

  • Adding mut everywhere. It hides which values really change. Start without mut and add it only when the compiler asks.
  • Expecting shadowing to change the outer variable. A shadow inside a block disappears at the end of the block; the outer value is untouched.
  • Leaving variables unused. The compiler warns about them. If a value is unused on purpose, start its name with an underscore, such as _reserved.
  • Forgetting the type on a const. const LIMIT = 10; is an error; write const LIMIT: u32 = 10;.

Key points

  • let creates a variable; Rust infers its type unless you add an annotation like : f64.
  • Variables are immutable by default; add mut when a value must change.
  • const values are fixed at compile time, need a type and use SCREAMING_SNAKE_CASE names.
  • Shadowing re-uses a name with a new let, can change the type and lasts until the end of the block.
  • mut changes a value in place and can never change its type.

Exercise

Write a program for a cricket over. Start with let mut score = 120; and add the runs from six balls: 1, 4, 0, 6, 2 and 1. Then shadow score with a string that says "Score: <number>" using format!, and print it. Also declare a constant OVERS_PER_INNINGS of type u32 with the value 20 and print it.

Show solution
Rust
const OVERS_PER_INNINGS: u32 = 20;

fn main() {
    let mut score = 120;
    score += 1;
    score += 4;
    score += 0;
    score += 6;
    score += 2;
    score += 1;

    let score = format!("Score: {score}");
    println!("{score}");
    println!("Overs per innings: {}", OVERS_PER_INNINGS);
}
Output
Score: 134
Overs per innings: 20

format! works like println! but returns the text as a String instead of printing it. The new score shadows the old number, so its type changes from an integer to a String.

Quick Quiz

1. What happens if you assign a new value to a variable declared with plain let?

2. Which declaration is correct for a constant?

3. What does this print? let x = 5; { let x = x * 3; } println!("{x}");

4. Why can shadowing change a variable's type but mut cannot?

Learn Rust with a mentor

Free lessons get you started. Our Programming & Software Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call