ZetCode

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.

main.rs
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.

main.rs
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.

main.rs
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.

main.rs
#[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.

main.rs
#[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.

main.rs
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.

main.rs
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:

main.rs
// 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:

main.rs
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:

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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.