Variables and Mutability
In this article, you will learn about variables and mutability in the Rust programming language — one of the most foundational concepts that sets Rust apart from other languages.
What Are Variables in Rust?
By default, all variables in Rust are immutable. This means once you bind a value to a variable name, that value cannot be changed.
This design choice is intentional. Rust encourages you to write code that is safe, predictable, and easy to reason about — especially in concurrent programs. Immutability by default reduces an entire class of bugs where a value gets unexpectedly changed in a different part of the program.
That said, Rust still gives you the option to make a variable mutable when you need to — you just have to be explicit about it.
Immutability by Default
Let’s see what happens when you try to reassign an immutable variable.
Open src/main.rs in your project and try this code:
fn main() {
let x = 5;
println!("The value of x is: {x}");
x = 6;
println!("The value of x is: {x}");
}Save the file and run it with:
cargo runYou will see a compile-time error:
Compiling rust_app v0.1.0 (/home/username/desktop-app/rust_app)
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
|
help: consider making this binding mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0384`
error: could not compile `rust_app` (bin "rust_app") due to 1 previous errorRust catches this at compile time, not at runtime. This is one of Rust’s greatest strengths — the compiler acts as a safety net that prevents you from making mistakes before the program ever runs.
The error clearly tells you that you tried to assign to x twice, and since x is immutable by default, Rust refuses to compile the program. It even suggests the fix: add the mut keyword.
This feature is especially valuable in large codebases. If you define a variable in one part of your code and another part accidentally tries to change it, Rust will stop you immediately.
Making a Variable Mutable
To allow a variable’s value to change, add the mut keyword after let:
fn main() {
let mut x = 21;
println!("The value of x is: {x}");
x = 22;
println!("The value of x is: {x}");
}Run it:
cargo runOutput:
Compiling rust_app v0.1.0 (/home/username/desktop-app/rust_app)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.81s
Running `target/debug/rust_app`
The value of x is: 21
The value of x is: 22By adding mut, you’re allowed to change the value bound to x from 21 to 22. Using mut also acts as a signal to other developers reading your code — it communicates that this variable’s value is intentionally designed to change.
Constants
Now that you understand variables, let’s look at constants. Both constants and immutable variables cannot be changed, but they are different in a few important ways:
| Feature | let (immutable) | const |
|---|---|---|
| Keyword | let | const |
Can use mut | Yes | No |
| Type annotation required | No (inferred) | Yes (always) |
| Allowed scope | Any | Any |
| Value set at | Compile time or runtime | Compile time only |
| Naming convention | snake_case | SCREAMING_SNAKE_CASE |
Constants are declared with the const keyword and must always have their type annotated. They cannot be used with mut. Constants are evaluated at compile time, not at runtime.
fn main() {
const THREE_HOURS_IN_SECONDS: i32 = 60 * 60 * 3;
println!("The value of constant is: {THREE_HOURS_IN_SECONDS}");
}The constant THREE_HOURS_IN_SECONDS follows Rust’s naming convention for constants — all uppercase with underscores separating words.
Constants Must Be Evaluated at Compile Time
Constants can only be set to a value that the compiler can compute at compile time. They cannot be set to the result of a function that runs at runtime.
This is not allowed — the function runs at runtime:
fn get_value() -> i32 {
42
}
const X: i32 = get_value(); // ❌ not allowed — runtime functionBut this is allowed — the function is marked const, so it is evaluated at compile time:
const fn get_value() -> i32 {
42
}
const X: i32 = get_value(); // ✅ allowed — const functionConstants are valid for the entire duration of the program, within the scope in which they are declared. They are useful for values that are meaningful across your entire application, such as the maximum number of connections or a fixed timeout duration.
Shadowing
Rust has another concept called shadowing, which is different from mutability. You can declare a new variable with the same name as an existing variable using let again. The second variable shadows the first — it takes over that name for any code that follows.
When you shadow a variable:
- The new variable replaces (shadows) the old one
- Any further use of that name refers to the new variable
- The original variable is effectively hidden (but still exists until its scope ends)
Here is an example:
fn main() {
let x = 5;
let x = x + 1; // shadows the previous x
{
let x = 10; // shadows outer x ONLY inside this block
println!("{}", x); // prints 10
}
println!("{}", x); // prints 6
}Output:
10
6Inside the inner block, x is shadowed to 10. Once that block ends, the inner shadow disappears and x returns to 6.
Shadowing vs mut — Key Difference
The most important difference between shadowing and mut is that shadowing allows you to change the type of a variable, while mut does not.
With shadowing, you can reuse the same name even for a different type:
fn main() {
let spaces = " "; // spaces is a &str (string)
let spaces = spaces.len(); // spaces is now a usize (number)
}This works because shadowing creates a new variable — it does not modify the original. You don’t need to invent a new name like spaces_str or spaces_count.
Compare this to using mut, which does not allow a type change:
fn main() {
let mut spaces = " "; // mutable string
spaces = spaces.len(); // ❌ ERROR: type mismatch
}Rust expected a &str but got a usize, so this fails at compile time. With mut, you can only reassign a value of the same type.
Use mut when you want to update a value of the same type. Use shadowing when you want to transform a value into a different type or apply a transformation while keeping the same name.
Summary
| Concept | Keyword | Can change value? | Can change type? |
|---|---|---|---|
| Immutable variable | let | No | No |
| Mutable variable | let mut | Yes | No |
| Constant | const | No | No |
| Shadowed variable | let (again) | Yes (new binding) | Yes |
What’s Next?
Now that you understand how variables work in Rust, you’re ready to explore Data Types — where you’ll learn about Rust’s scalar and compound types, and how type annotations work in practice.