Variables and Mutability

Understand how Rust handles variables - immutability by default, explicit mutability, constants, and shadowing - and why these rules prevent bugs before your code ever runs.

Rust treats variables differently from almost every mainstream language. In Python, JavaScript, or Go, a variable you create with x = 5 is ready to be changed later with x = 6. Rust flips that assumption on its head. By default, a value bound to a name cannot be altered. That single design choice eliminates an entire category of bugs that plague codebases written in languages where mutation is the default.

The language then builds a precise set of tools around that decision: the let keyword for binding names, the mut keyword for explicitly opting into mutation, const for compile-time constants, and shadowing for transforming values while keeping names immutable. Each of these serves a different need, and knowing which one to reach for is one of the first real skills a Rust programmer develops.

This page introduces all four concepts as a foundation for the rest of the language. The individual sections that follow in this chapter dive into each one with full detail, but the overview here gives you the mental model you need to navigate them.

Immutable by Default

When you write let x = 5; in Rust, the compiler commits to keeping x bound to the value 5 for the rest of that binding’s lifetime. You cannot later write x = 6; — the compiler will refuse with a clear error:

error[E0384]: cannot assign twice to immutable variable `x`

This is the exact opposite of JavaScript’s let, Python’s assignment, or Go’s var. In those languages, you must opt into immutability with const or naming conventions. In Rust, you opt out of it with mut.

Why does the language work this way? Because accidental mutation is a major source of bugs, especially in large programs or concurrent systems. When you see a variable x in Rust, you know it represents the same value everywhere it appears. The compiler enforces that promise. You never have to trace through a function to check whether something changed x halfway through — it didn’t.

Think in bindings, not boxes:

A helpful mental model: a let statement creates a binding between a name and a value, not a box that holds a value. Immutable bindings are like sticky notes you write once and never overwrite. If you need to change what the name refers to later, you either make the binding mutable or create a new binding (shadowing).

For someone coming from a language where everything is mutable by default, this rule feels restrictive at first. But the Rust compiler is not trying to slow you down — it is preventing an entire class of problems that you no longer need to think about. In practice, most variables you create genuinely do not need to change after they are set. Rust simply makes that the safe, compiler-checked default.

Opting Into Change with mut

When you do need a value that can change — a counter, an accumulator, a flag — you tell the compiler by adding the mut keyword:

let mut count = 0;
count = count + 1;
println!("Count is now: {}", count);

The keyword mut makes the binding mutable, meaning you can reassign a new value to the same name. The type of the variable stays the same; you cannot assign a string to a name that was originally bound to an integer unless you use shadowing instead.

mut only allows reassignment of the same type:

Making a variable mutable with mut does not let you change its type. This program will not compile:

let mut x = 5;
x = "hello"; // error: expected integer, found &str

If you need to change the type while keeping the name, shadowing is the tool for the job.

The existence of mut does more than just allow change. It serves as documentation. When you read code and see let mut, you immediately know that this variable is meant to be updated. When you do not see mut, you can trust that the value is stable. This makes code reviews and debugging far simpler — the author’s intention about mutation is stated explicitly in the source, not buried in a comment or a convention.

Constants with const

Rust also has a separate concept called constants, declared with the const keyword instead of let. Constants are always immutable — mut is not an option — and they must be assigned a value that the compiler can compute at build time.

const MAX_CONNECTIONS: u32 = 100;
const WELCOME_MESSAGE: &str = "Hello, world!";

The key difference between a constant and an immutable let binding is when the value is known. An immutable let binding can hold the result of a function call or any expression computed while the program runs. A constant must be a compile-time expression — no function calls, no heap allocations, nothing that depends on runtime input.

This distinction matters for performance and for where you can use them. Constants can appear in any scope, including at the module level or inside functions, and they are inlined by the compiler. They are also the only choice for values that need to be known at compile time, such as array sizes.

When to use const:

Use const when the value is truly fixed and known before the program runs — configuration limits, mathematical constants, or static strings that never change. The compiler can optimize these aggressively, and readers immediately understand that this value is baked into the binary.

Constants use SCREAMING_SNAKE_CASE naming by convention, and the type annotation is mandatory. The compiler will not infer it for you.

Shadowing - A New Binding With the Same Name

Shadowing is a Rust feature that looks like reassignment but is actually something fundamentally different. When you write:

let x = 5;
let x = x + 1;
let x = "now a string";

You are not changing x. You are creating a new binding with the same name that overshadows the previous one. The old binding still exists for its original lifetime, but the name now points to the new value.

This is what lets the third line work — x changes from an integer to a string. That is impossible with mut, because mut keeps the same binding and requires the same type. Shadowing creates a new, unrelated binding that simply happens to share the name.

Shadowing is not mutation:

A common mistake is thinking that shadowing modifies the original variable. It does not. If any code still holds a reference to the original binding (for example, in a closure or a reference stored elsewhere), that original value remains unchanged. Shadowing only affects what the name x resolves to from that point forward.

Shadowing shines when you are transforming a value through multiple steps — parsing input, trimming whitespace, converting types — and you want to avoid cluttering the namespace with names like raw_input, trimmed_input, parsed_input. You can reuse the same name for each stage while keeping every intermediate binding immutable.

let input = "   42   ";
let input = input.trim();        // still a &str
let input: u32 = input.parse()   // now a u32
    .expect("input must be a number");

Each let creates a new binding. None of them can be accidentally modified later. This pattern is common in Rust codebases and is considered idiomatic.

Choosing the Right Tool

A single page introduces four related but distinct mechanisms. The decision of which to use depends on what you need:

  • The value never changes and is known at compile timeconst.
  • The value never changes but is computed at runtime → an immutable let binding.
  • The value needs to change, same typelet mut.
  • The value needs to be transformed, possibly changing type, while staying immutable → shadowing with new let bindings.

Most of the time, you start with an immutable let. The compiler will gently remind you to add mut or to reach for shadowing when the situation calls for it.

The compiler is your pair programmer:

If you try to mutate a non-mut variable, Rust gives you a clear error message that often suggests adding mut. Pay attention to those messages — they teach you the language's expectations one step at a time.

Summary

Rust’s approach to variables is different because it treats immutability as the normal state of affairs and forces mutation to be declared explicitly. This shifts the burden of safety from runtime to compile time. You declare bindings with let, make them mutable with mut only when necessary, use const for compile-time-known values, and apply shadowing to transform data without sacrificing immutability.