Introduction
Rust takes an opinionated stance on variables: they are immutable by default. This might feel restrictive if you come from languages like JavaScript or Python, but it is a deliberate design choice that makes code safer and easier to reason about. Understanding mutability, constants, and shadowing is essential before you write any real Rust code.
Key Concepts
- Immutable variable: A binding declared with
letthat cannot be reassigned. This is the default in Rust. - Mutable variable: A binding declared with
let mutthat can be reassigned to a new value of the same type. - Constant: A value declared with
constthat is always immutable, requires a type annotation, and must be a compile-time expression. - Shadowing: Declaring a new variable with the same name as a previous one using
let, which creates a fresh binding that can even change the type.
Real World Context
Immutability by default prevents an entire category of bugs where a variable is accidentally modified in a distant part of the code. In a large codebase, seeing let mut immediately signals that a value will change, making code reviews faster and intent clearer. Shadowing is used constantly in real Rust code for transforming values through a pipeline while keeping meaningful names.
Deep Dive
In Rust, variables are immutable by default. The compiler rejects any attempt to reassign them:
rustlet x = 5; // x = 6; // Error! Cannot assign twice to immutable variable
To opt into mutability, add the mut keyword:
rustlet mut y = 10; y = 15; // OK y += 1; // Also OK
Constants are different from immutable variables in several important ways:
rustconst MAX_POINTS: u32 = 100_000; // MUST have type annotation const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3; // Computed at compile time
Constants require type annotations, can only be set to constant expressions, can never use mut, and can be declared in any scope including global scope.
Shadowing is one of Rust's most powerful patterns. You can declare a new variable with the same name using let, which creates an entirely new binding:
rustlet spaces = " "; // &str let spaces = spaces.len(); // usize - type changed!
This is different from mutation. With mut, you cannot change the type:
rustlet mut spaces = " "; // spaces = spaces.len(); // Error! Can't change type with mut
Shadowing is especially useful when transforming a value through several steps or converting types while keeping a meaningful name:
rustlet guess: u32 = "42".parse().expect("Not a number");
Common Pitfalls
- Confusing shadowing with mutation — Shadowing creates a completely new variable that happens to have the same name. It can change the type and does not require
mut. Mutation modifies the existing variable in place and cannot change its type. - Forgetting
mutwhen you need it — If you need to modify a variable, declare it withlet mutfrom the start. The compiler error message will remind you, but anticipating it saves time. - Using
mutwhen shadowing would be cleaner — If you only need to transform a value once, shadowing withletis more idiomatic than usinglet mutand reassigning.
Best Practices
- Default to immutability — Only add
mutwhen you genuinely need to modify the variable. This makes your intent explicit and helps the compiler optimize. - Use shadowing for type transformations — When parsing a string into a number or processing data through stages, shadow the variable rather than inventing new names like
input_strandinput_num.
Summary
- Variables in Rust are immutable by default; use
let mutto opt into mutability. - Constants (
const) require type annotations and must be compile-time expressions. - Shadowing with
letcreates a new variable, allowing type changes unlikemut. - Immutability by default makes code safer and intent clearer.
- Prefer shadowing over mutation when transforming a value through stages.
Code Examples
fn main() {
let x = 5;
let x = x + 1; // Shadow with new value
{
let x = x * 2; // Inner scope shadow
println!("Inner x: {x}"); // 12
}
println!("Outer x: {x}"); // 6
}