Rust Mutability
last modified April 22, 2026
In this article we explore mutability in Rust, a fundamental concept that ensures memory safety and prevents data races at compile time.
Rust takes a unique approach to mutability: variables are immutable by default. This design choice helps prevent bugs, enables safer concurrent programming, and makes code behavior more predictable.
Immutable Variables by Default
In Rust, when you bind a value to a variable, that variable cannot be changed unless you explicitly mark it as mutable.
fn main() {
let x = 5;
println!("The value of x is: {}", x);
// This will cause a compile-time error:
// x = 6;
println!("x is still: {}", x);
}
In the example, x is immutable. Attempting to reassign it results
in a compiler error, catching potential bugs before runtime.
let x = 5; // x = 6; // error[E0384]: cannot assign twice to immutable variable
The Rust compiler enforces immutability, preventing accidental modifications and making code easier to reason about.
λ cargo run -q The value of x is: 5 x is still: 5
The mut Keyword
To make a variable mutable, add the mut keyword after let.
fn main() {
let mut counter = 0;
println!("Counter: {}", counter);
counter += 1;
println!("Counter: {}", counter);
counter *= 3;
println!("Counter: {}", counter);
}
Here, counter is declared mutable, allowing us to modify its value.
let mut counter = 0; counter += 1;
The mut keyword applies to the binding, not the value itself.
This distinction becomes important with complex types and references.
λ cargo run -q Counter: 0 Counter: 1 Counter: 3
Best Practice: Start with immutable bindings and add
mut only when necessary. This makes your intent clear and helps
the compiler catch unintended modifications.
Mutability and References
Rust's borrowing system distinguishes between immutable references (&T)
and mutable references (&mut T). You can have many immutable
references OR one mutable reference, but not both simultaneously.
fn main() {
let mut data = vec![1, 2, 3];
// Immutable borrow - multiple allowed
let r1 = &data;
let r2 = &data;
println!("r1: {:?}, r2: {:?}", r1, r2);
// Mutable borrow - exclusive access
let r3 = &mut data;
r3.push(4);
// Cannot use r1 or r2 here while r3 is active:
// println!("{:?}", r1); // error[E0502]
println!("data after push: {:?}", r3);
}
This example demonstrates Rust's borrowing rules. Immutable references allow reading, while mutable references allow modification—but only one at a time.
let r1 = &data; // immutable borrow let r3 = &mut data; // mutable borrow - exclusive
The compiler ensures that mutable references don't alias with other references, preventing data races at compile time.
λ cargo run -q r1: [1, 2, 3], r2: [1, 2, 3] data after push: [1, 2, 3, 4]
Mutable Methods and self
Methods that modify a struct's fields take &mut self as their
first parameter. This indicates the method requires mutable access to the instance.
#[derive(Debug)]
struct Counter {
count: u32,
}
impl Counter {
fn new() -> Self {
Counter { count: 0 }
}
// Immutable method - can read but not modify
fn get(&self) -> u32 {
self.count
}
// Mutable method - can modify the struct
fn increment(&mut self) {
self.count += 1;
}
fn reset(&mut self) {
self.count = 0;
}
}
fn main() {
let mut c = Counter::new();
println!("Initial: {}", c.get());
c.increment();
c.increment();
println!("After increments: {}", c.get());
c.reset();
println!("After reset: {}", c.get());
}
The increment and reset methods take
&mut self, allowing them to modify the Counter
instance. The get method takes &self, providing
read-only access.
fn increment(&mut self) {
self.count += 1;
}
The &mut self syntax is shorthand for
self: &mut Self. It's required for any method that changes
the struct's state.
λ cargo run -q Initial: 0 After increments: 2 After reset: 0
Mutability in Structs
Even if a struct instance is mutable, its fields follow the same rules:
you need mut on the binding to modify fields.
#[derive(Debug)]
struct User {
name: String,
active: bool,
}
fn main() {
// Immutable binding - cannot modify fields
let user1 = User {
name: String::from("Alice"),
active: true,
};
// user1.active = false; // error: cannot assign
// Mutable binding - can modify fields
let mut user2 = User {
name: String::from("Bob"),
active: true,
};
user2.active = false;
println!("{:?}", user2);
}
The mut keyword on the binding allows modification of the struct's
fields. Without it, the entire struct is read-only.
let mut user2 = User { ... };
user2.active = false; // OK because user2 is mutable
Note: If a field itself contains a mutable reference or interior-mutable type
(like Cell or RefCell), it can be modified even
through an immutable binding—this is called interior mutability.
λ cargo run -q
User { name: "Bob", active: false }
Shadowing vs Mutability
Rust allows shadowing: declaring a new variable with the same name as a previous one. This is different from mutation and allows type changes.
fn main() {
// Shadowing example
let x = "hello";
let x = x.len(); // x is now usize
let x = x * 2; // x is still usize
println!("Shadowed x: {}", x);
// Mutability example
let mut y = 10;
y = y + 5; // y is still i32
// y = "text"; // error: type mismatch
println!("Mutated y: {}", y);
}
Shadowing creates a new variable that hides the previous one. This allows transforming values and even changing types. Mutation changes the value of the same variable without changing its type.
let x = "hello"; let x = x.len(); // Shadowing: new variable, new type
let mut y = 10; y = y + 5; // Mutation: same variable, same type
Use shadowing for transformations and type conversions. Use mut
when you need to update a value in place.
λ cargo run -q Shadowed x: 10 Mutated y: 15
Interior Mutability Pattern
Sometimes you need to mutate data even when you only have an immutable reference.
Rust provides types like Cell<T> and RefCell<T>
for interior mutability—mutability enforced at runtime instead of compile time.
use std::cell::RefCell;
#[derive(Debug)]
struct Data {
value: RefCell<i32>,
}
fn main() {
let data = Data {
value: RefCell::new(10)
};
// Even though `data` is immutable, we can modify `value`
*data.value.borrow_mut() += 5;
println!("Value: {}", data.value.borrow());
// Multiple immutable borrows of the RefCell content
let r1 = data.value.borrow();
let r2 = data.value.borrow();
println!("r1: {}, r2: {}", r1, r2);
// A mutable borrow would panic if immutable borrows are active:
// let m = data.value.borrow_mut(); // panic!
}
RefCell<T> enables mutable access through an immutable binding.
Borrowing rules are checked at runtime, panicking if violated.
*data.value.borrow_mut() += 5;
borrow_mut() returns a mutable reference to the inner value.
This is checked at runtime—if immutable borrows exist, the program panics.
λ cargo run -q Value: 15 r1: 15, r2: 15
When to use: Interior mutability is useful for implementing mock objects in tests, building data structures with shared ownership (like graphs), or when working with APIs that require immutable references but need internal state changes.
Mutability Best Practices
Following these guidelines leads to safer, more maintainable Rust code:
- Prefer immutability: Start with
let, addmutonly when the compiler requires it. - Limit mutable scope: Keep mutable references active for the shortest time possible to reduce borrowing conflicts.
- Use interior mutability intentionally: Reach for
Cell/RefCellonly when immutable references must allow mutation—document why. - Avoid mutable statics: Use thread-safe alternatives
like
AtomicUsize,Mutex, or theonce_cellcrate. - Make intent explicit: Use
mutto signal to readers that a value will change.
// Good: immutable by default
fn process(data: &[i32]) -> i32 {
data.iter().sum()
}
// Good: mut only where needed
fn count_positives(numbers: &[i32]) -> usize {
let mut count = 0; // mut required for incrementing
for &n in numbers {
if n > 0 {
count += 1;
}
}
count
}
// Good: limit mutable borrow scope
fn update_first(vec: &mut Vec<i32>) {
if let Some(first) = vec.first_mut() {
*first += 1; // mutable borrow ends here
}
// Can now immutably borrow `vec` again
println!("First element updated");
}
These examples demonstrate applying mutability principles: minimal
mut, clear intent, and scoped mutable borrows.
Common Pitfalls
Here are frequent mistakes when working with mutability in Rust:
fn main() {
// Pitfall 1: Forgetting mut on binding
let mut data = vec![1, 2, 3];
let ref_mut = &mut data;
ref_mut.push(4); // OK
// Pitfall 2: Holding mutable borrow too long
let mut numbers = vec![10, 20, 30];
{
let m = &mut numbers;
m.push(40);
// m goes out of scope here
}
// Now we can borrow immutably again
println!("{:?}", numbers);
// Pitfall 3: Confusing shadowing with mutation
let x = 5;
let x = x + 1; // Shadowing, not mutation
// Original x is gone; new x hides it
// Pitfall 4: Mutating through multiple references
let mut value = 100;
let r1 = &mut value;
// let r2 = &mut value; // Error: cannot borrow twice
*r1 = 200;
// Now r1's borrow ends, can borrow again
}
Understanding these patterns helps avoid compiler errors and write more idiomatic Rust.
λ cargo run -q [10, 20, 30, 40]
Conclusion
Mutability in Rust is a powerful feature designed with safety in mind. By making variables immutable by default and requiring explicit opt-in for mutation, Rust prevents entire classes of bugs at compile time.
Key takeaways:
- Use
let mutonly when you need to modify a binding. - Understand borrowing rules: one mutable OR many immutable references.
- Use interior mutability (
Cell,RefCell) for special cases requiring runtime-checked mutation. - Avoid
static mut; prefer safe concurrency primitives. - Let the compiler guide you—it will tell you exactly where
mutis needed.
Embracing Rust's mutability model leads to code that is not only safe and concurrent by default, but also clearer and easier to maintain.
Author
List all Rust tutorials.