Rust · Beginner

Variables and Mutability in Rust

7 min readUpdated September 24, 2026Every example verified

In short: A Rust variable is created with let and cannot be reassigned unless it is declared let mut. A second let with the same name shadows the first and may change its type, and const declares a compile-time value that never changes.

Immutable by default, and why

In Rust, let count = 3; creates a binding that can never be changed. Trying to write count = 4; later is a compile-time error, not a warning. To allow reassignment you say so at the declaration: let mut count = 3;. This is the reverse of most languages, where everything is mutable unless you add a keyword such as final, and the reversal is deliberate. When you read Rust code, every mut marks a value that may change over time, so anything without it can be relied on to stay as it was. The compiler relies on the same promise: a value that never changes can be shared freely, which becomes central in the ownership lesson.

Declaring a variable mut does not change its type; it changes what you are allowed to do with the binding. count += 1 needs mut; so does calling a method that modifies the value in place, such as push_str on a String. If you add mut and never mutate, the compiler warns about an unused mut, which is a hint to remove it.

The compiler infers the type of most variables from the value, so let count = 3; is an i32 and let price = 3.5; is an f64. Add an annotation when the value does not settle the type, or when you want a different one: let seats: u32 = 40;. You can also declare a variable without a value, let total;, and assign it exactly once later; using it before that assignment is an error, so there is no such thing as an uninitialised variable in Rust.

Rust lets you declare a new variable with the same name as an existing one. This is called shadowing. The second let creates a fresh binding that hides the first for the rest of its scope; it is not an assignment, so it works on immutable variables and the new variable may even have a different type. Shadowing is the idiomatic way to turn the text read from input into a number: let line: u32 = line.trim().parse().unwrap(); reuses the good name instead of inventing line_text and line_number. A shadow declared inside a block disappears at the block's closing brace, and the outer variable is visible again.

A const is different from an immutable variable. Its value must be fixed at compile time, it must carry a type annotation, its name is written in upper snake case, and it can be declared outside any function so that every function can use it. The compiler substitutes the value at each use. A static also lives for the whole program but occupies one fixed memory location; you rarely need one, and a mutable static requires unsafe, so prefer const for fixed numbers and text.

Syntax

 Rust · syntax
let capacity = 40;            // immutable, type inferred (i32)
let mut aboard = 0;           // mutable
aboard += 12;                 // allowed because of mut

let seats: u32 = 40;          // explicit type
let label;                    // declared now...
label = "ferry";              // ...assigned exactly once later

let line = "  7 ";
let line = line.trim();                // shadowing: a new variable, same name
let line: u32 = line.parse().unwrap(); // shadowing again, with a new type

const MAX_CRATES: u32 = 12;   // compile-time constant; the type is required

let introduces a variable, mut allows it to be reassigned or modified in place, and repeating let with an existing name shadows the earlier variable rather than assigning to it.

A counter that changes and a limit that does not

Only aboard is declared mut, because only aboard is modified after its declaration.

 Rust
fn main() {
    let capacity = 40;
    let mut aboard = 0;
    println!("Boarding started, {} aboard", aboard);
    aboard += 12;
    aboard += 9;
    println!("After two groups: {} aboard", aboard);
    let free = capacity - aboard;
    println!("Seats still free: {}", free);
}

Output

Boarding started, 0 aboard
After two groups: 21 aboard
Seats still free: 19

capacity never changes, so it is declared without mut; if a later line tried to assign to it, the program would not compile. aboard is declared mut because the two += lines modify it. free is computed once from the other two values and is immutable as well. Reading the declarations tells you, before you read the rest of the function, which values can move.

Shadowing to turn input text into a number

The program is run with the input line 7 (with spaces around the digit).

 Rust
use std::io::BufRead;

fn main() {
    let mut lines = std::io::stdin().lock().lines();
    let crates = lines.next().unwrap().unwrap();
    println!("Read the text {:?}", crates);
    let crates: u32 = crates.trim().parse().unwrap();
    let crates = crates * 24;
    println!("Bottles: {}", crates);
}

Input given to the program: 7

Output

Read the text "  7 "
Bottles: 168

The first crates holds the raw text from standard input, printed with {:?} so the surrounding spaces are visible inside quotes. The second let crates shadows it with a u32 parsed from the trimmed text; the type annotation tells parse() what to produce. The third let crates shadows again with the multiplied value. None of these lines is an assignment, so none needs mut, and at the end there is one live variable named crates with the type you want. This is the standard pattern for reading numbers in every exercise.

Constants and block scope

Watch what the last line prints after the inner block has shadowed crates.

 Rust
const CRATES_PER_PALLET: u32 = 12;
const DEPOT: &str = "North Quay";

fn main() {
    let pallets = 3;
    let crates = pallets * CRATES_PER_PALLET;
    println!("{}: {} crates", DEPOT, crates);
    {
        let crates = crates + 5;
        println!("Inside the block: {} crates", crates);
    }
    println!("After the block: {} crates", crates);
}

Output

North Quay: 36 crates
Inside the block: 41 crates
After the block: 36 crates

CRATES_PER_PALLET and DEPOT are constants: declared outside main with explicit types, usable anywhere in the file, fixed at compile time. Inside the inner block, let crates = crates + 5; creates a second crates that shadows the first; the right-hand side still reads the outer one. When the block ends, that shadow is gone and the final line prints the outer value again. Shadowing never modifies the original binding.

Common mistakes

  • Assigning to a variable declared without mut

    Why it goes wrong: The compiler reports error E0384, cannot assign twice to immutable variable. let alone is a promise that the value will not change, and the compiler enforces it.

    Fix: Add mut at the declaration if the value genuinely changes, or compute the new value into a new variable.

     Rust · fix
    let mut total = 0;
    total += 5;
  • Writing a const without a type

    Why it goes wrong: const LIMIT = 12; is rejected with missing type for const item. Unlike let, a constant's type is never inferred, because a constant can be used from anywhere in the file and the compiler wants its meaning fixed at the declaration.

    Fix: Write the type: const LIMIT: u32 = 12;.

  • Expecting a shadow inside a block to change the outer variable

    Why it goes wrong: A let inside braces creates a new variable that exists only until the closing brace. The outer variable is untouched, so code after the block sees the old value.

    Fix: To update the outer value, declare it mut and assign with = or += inside the block instead of writing let.

     Rust · fix
    let mut crates = 36;
    {
        crates += 5;
    }
    println!("{}", crates); // 41
  • Using a variable outside the block where it was declared

    Why it goes wrong: Variables live until the end of the block that declares them. Referring to one after that block gives error E0425, cannot find value in this scope, and the compiler notes that the binding exists in a different scope.

    Fix: Declare the variable before the block, at the level where it is needed, and assign to it inside.

let, let mut, shadowing and const

FormCan be reassigned?Can change type?When the value is fixed
let x = 5;nonoat run time, once
let mut x = 5;yesnochanges while the program runs
let x = ...; let x = ...;each let is a new variableyesat each declaration
const X: u32 = 5;nonoat compile time

Where you use this

Reading configuration-like values at the top of a program is where the three forms meet. A tool that packs bottles into crates declares const BOTTLES_PER_CRATE: u32 = 24; because that number is a fact about the crates, not something the program computes. It reads the number of bottles from input into a text variable, then shadows it with the parsed u32, so the rest of the code never sees the raw text. The running total of crates packed is the only value that changes, so it is the only let mut. When a colleague opens the file, that single mut tells them exactly where the state of the program lives.

 Rust · in practice
const BOTTLES_PER_CRATE: u32 = 24;

let bottles = line.trim().parse::<u32>().unwrap();
let mut crates_packed = 0;

Key points

  • let creates an immutable binding; reassigning it is a compile-time error (E0384).
  • let mut allows reassignment and in-place modification; the compiler warns if mut is unused.
  • Types are inferred from the value; add : Type when the value does not decide it.
  • A second let with the same name shadows the first and may change the type; it is not an assignment.
  • A shadow declared in a block ends with the block.
  • const NAME: Type = value; is fixed at compile time, needs its type, and is written in upper snake case.

Try it yourself

The program prints the number of pallets. Below that line, add a mutable total that starts at 0, add pallets * CRATES_PER_PALLET to it, then add 5 loose crates, and print Total crates: followed by the value.

Your program
const CRATES_PER_PALLET: u32 = 12;

fn main() {
    let pallets = 3;
    println!("Pallets: {}", pallets);
}
Expected output: Pallets: 3 Total crates: 41

Practise this

Open the Rust playground

Frequently asked questions

Is a Rust variable declared with let a constant?

No. An immutable let binding gets its value at run time, for example from input, and simply cannot be reassigned afterwards; a const must be computable by the compiler and is substituted at each use. Both never change, but only const can be declared outside a function and used in places that need a compile-time value, such as an array length.

Why does Rust allow shadowing when it forbids reassignment?

Because they are different operations. Reassignment changes the value behind an existing name, which the compiler must be able to track for safety. Shadowing creates a new variable and merely reuses the name, so nothing that already referred to the old variable is affected. It lets you transform a value through a few steps, such as trimming and parsing input, without inventing a new name for each step.

Does mut change where or how a variable is stored?

No. mut is a property of the binding, not of the value or its storage. A let mut integer and a let integer occupy the same kind of memory; the difference is entirely in what the compiler lets you do with the name. This is why moving a value into a new let mut binding makes it mutable again.

Progress is stored only in this browser.

How this page was checked. Every program on it was run with rustc 1.94 at build time by the publishing checks, and the output shown is what it printed. Running Rust inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with rustc 1.94 locally.