rust-variables-mutabilidad-shadowing

Variables and Mutability in Rust: let, mut, and Shadowing

  • 6 min

A variable in Rust is a name associated with a value that, by default, cannot be modified.

In Rust, declaring a variable does not automatically mean “I’ll be able to change this later.” The default decision is the opposite: if something doesn’t need to change, it doesn’t change.

This choice may seem restrictive at first, but it has a very clear intention. It makes code more predictable, easier to read, and less prone to subtle errors.

In this article, we’ll see how to declare variables with let, when to use mut, what the difference is with const, and how a very practical Rust feature called shadowing works.

Declaring Variables with let

To create a variable in Rust, we use the keyword let.

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);
}
Copied!

So far, everything seems normal. Rust is a statically typed language, meaning it must know the types of all variables at compile time. However, we didn’t write let x: i32 = 5;.

This is thanks to type inference. Rust is smart enough to deduce that if we assign a 5, we probably want an integer (by default i32).

The “Problem” of Immutability

Now, let’s try to do what we would do in any other language: change the value.

fn main() {
    let x = 5;
    println!("The value of x is: {}", x);

    x = 6; // Error!
    println!("The value of x is: {}", x);
}
Copied!

If you try to compile this (cargo run), Rust won’t just throw an error—the compiler will explain exactly what’s happening with a very descriptive message:

error[E0384]: cannot assign twice to immutable variable `x`
 --> src/main.rs:4:5
  |
2 |     let x = 5;
  |         - first assignment to `x`
3 |     println!("The value of x is: {}", x);
4 |     x = 6;
  |     ^^^^^ cannot assign twice to immutable variable
Copied!

It tells us: “You cannot assign twice to the immutable variable x.”

Why does Rust do this?

Having immutable variables by default makes code easier to reason about. If you see let x = 5, you have the guarantee that x will be 5 throughout its scope.

Additionally, it’s fundamental for concurrency safety: if a piece of data doesn’t change, multiple threads can read it simultaneously without fear of race conditions.

Mutability with mut

Still, a program where nothing changes is usually not very useful. Sometimes we need to modify values (a counter, a position in a game, etc.).

To make a variable mutable, we must be explicit and add the keyword mut before the variable name.

fn main() {
    let mut x = 5;
    println!("The value of x is: {}", x);

    x = 6;
    println!("The value of x is: {}", x);
}
Copied!

Now the code compiles and works perfectly.

By adding mut, we are communicating two things:

  1. To the compiler: “Allow me to reassign this variable.”
  2. To other programmers (or to your future self): “Be careful, this variable’s value will change throughout the function.”

Constants (const)

If variables are immutable by default, how are they different from constants?

In Rust, we have the keyword const, but it has very different rules from an immutable let:

  1. Explicit type required: You cannot rely on inference. You must write the type (e.g., : u32).
  2. Only constant expressions: Its value must be computable at compile time, not at runtime. You cannot assign to a constant the result of a function that reads a file, for example.
  3. Global scope: Unlike variables, constants can be declared outside the main function, in the module’s global scope.
  4. Immutable forever: There is no const mut.

By convention, Rust constants are written in SCREAMING_SNAKE_CASE.

const MAX_POINTS: u32 = 100_000;
const SECONDS_IN_HOUR: u32 = 60 * 60; // This is valid, computed at compile time

fn main() {
    println!("The maximum number of points is: {}", MAX_POINTS);
}
Copied!

Use const for values that are truly fixed in the universe of your program (Pi, configuration limits, physical constants). Use let for everything else.

Shadowing

In Rust, you can declare a new variable with the same name as a previous one. The new variable “shadows” (hides) the previous one: this is shadowing.

fn main() {
    let x = 5;

    // Here we redeclare 'x'. The previous one is no longer accessible.
    let x = x + 1;

    {
        // Shadowing within an inner scope
        let x = x * 2;
        println!("The value of x in the inner scope is: {}", x); // 12
    }

    println!("The value of x in the outer scope is: {}", x); // 6
}
Copied!

Notice that we use let each time.