Skip to main content
  1. Posts/

Rust basics: let, mut, and shadowing

·481 words·3 mins
Author
m58
0xm58.xyz
Table of Contents
Rust variables are really bindings. Once that clicks, let, mut, and shadowing stop feeling like syntax trivia and start feeling like design choices.

Rust pushes you toward immutability by default. That is not just style. It reduces accidental state changes and makes data flow easier to follow.

let creates an immutable binding
#

let x = 10;
// x = 11; // error

The value is not frozen forever in the universe. What Rust is saying is simpler: this binding cannot be reassigned.

That default is useful because most values in most programs do not actually need to change.

mut allows reassignment
#

If a binding really should change, mark it as mutable:

let mut count = 0;
count += 1;
count += 1;

Use mut deliberately. When every variable is mutable by habit, it becomes harder to reason about which state transitions are real and which are incidental.

Shadowing creates a new binding
#

Shadowing means reusing the same name for a new binding:

let x = 10;
let x = x + 1;
let x = x.to_string();

That final x is not the original integer with some weird mutation history. It is a brand-new binding, and it even has a different type.

This is one of the most useful beginner patterns in Rust.

Mutation versus shadowing
#

These two are different in a way that matters:

let mut name = String::from("ada");
name.push_str(" lovelace");

This mutates the same value.

let name = "ada";
let name = name.trim();
let name = name.to_uppercase();

This creates a sequence of new bindings derived from the previous value.

When you are transforming data step by step, shadowing often reads better than making one mutable variable carry several meanings.

A common pattern: parse and reuse the name
#

let input = "42";
let input = input.parse::<i32>().unwrap();

The name stays relevant, but the meaning has evolved from “raw string input” to “parsed integer value.”

That is a good use of shadowing.

Scope still matters
#

Bindings live only within their scope:

let result = {
    let temp = 2;
    temp * 5
};

temp disappears after the block. result keeps the computed value.

This is worth remembering because Rust code often uses small blocks to keep temporary values short-lived.

When to choose which
#

Use plain let when the value should not change:

let hostname = "0xm58.xyz";

Use mut when one piece of state genuinely evolves:

let mut retries = 0;
retries += 1;

Use shadowing when a value is being refined or converted:

let path = " /tmp/file.txt ";
let path = path.trim();
let path = path.to_string();

Summary
#

  • let creates an immutable binding by default.
  • mut lets you reassign the same binding.
  • Shadowing creates a new binding, even if the name stays the same.

In practice, good Rust code usually prefers:

  • immutable bindings first
  • mutation only when state must evolve
  • shadowing for clean, linear transformations

Related

Rust basics: control flow

·665 words·4 mins
Rust control flow is more than branching and loops. The important part is that many control-flow forms are expressions, which means they produce values. That design shows up everywhere in day-to-day Rust. It makes code concise, but it also forces you to be explicit about the shapes of the values you return.

Rust basics: functions and types

·598 words·3 mins
Rust functions are explicit at the boundary and flexible inside the body. You spell out parameter and return types, then let inference do most of the local work. That balance is one of Rust’s better design choices. Signatures stay readable, but the code inside them does not turn into annotation noise.

Rust basics: structs and enums

·587 words·3 mins
Structs and enums are the core of Rust data modeling. Structs group fields together. Enums describe a value that can be one of several well-defined variants. If you are coming from languages where data modeling is mostly “objects everywhere”, Rust feels different at first. It prefers explicit data shapes plus separate behavior.