Variables & Mutability
Learn about let bindings, mutability, shadowing, and constants in Rust.
Immutable by Default
In Rust variables are immutable by default, which means once a value is bound to a name you cannot change it. This design choice encourages you to write code that is easier to reason about and naturally safe for concurrency. The compiler will produce a clear error message if you try to mutate an immutable variable. This is one of the many ways Rust pushes you toward writing correct code from the start.
fn main() {
let x = 5;
println!("The value of x is: {}", x);
// This would cause a compile error:
// x = 6; // error[E0384]: cannot assign twice to immutable variable
// Instead, use mut for mutable variables
let mut y = 10;
println!("y is: {}", y);
y = 20;
println!("y is now: {}", y);
}
The mut Keyword
When you do need a variable to change, you explicitly mark it with the mut keyword. This communicates your intent to anyone reading the code that this variable's value will be modified later. The compiler tracks mutability and ensures that references respect the mutability rules. Marking variables as mutable only when necessary makes it clear which parts of your code change state.
fn main() {
let mut counter = 0;
let target = 5;
while counter < target {
counter += 1;
println!("Counter: {}", counter);
}
let mut name = String::from("Hello");
name.push_str(", Rust!");
println!("{}", name);
// Mutable references
let mut value = 42;
let r = &mut value;
*r += 8;
println!("Modified value: {}", value);
}
Shadowing
Rust allows you to declare a new variable with the same name as a previous variable, which is called shadowing. The new variable shadows the previous one, meaning subsequent code will see the new value. Unlike mutation, shadowing lets you change the type of a value while reusing the same name. This is useful for transforming data through a series of steps without inventing new variable names for each intermediate result.
fn main() {
let x = 5;
let x = x + 1; // shadows the first x
let x = x * 2; // shadows the second x
println!("x = {}", x); // prints 12
// Shadowing allows type changes
let spaces = " ";
let spaces = spaces.len();
println!("Number of spaces: {}", spaces);
// This would NOT work with mut:
// let mut text = "hello";
// text = text.len(); // error: mismatched types
// Shadowing in inner scopes
let outer = 10;
{
let outer = outer + 5;
println!("Inner scope: {}", outer); // 15
}
println!("Outer scope: {}", outer); // 10
}
Constants
Constants are values that are bound to a name and are never allowed to change. Unlike let bindings, constants must have their type annotated explicitly and their value must be a constant expression that can be evaluated at compile time. Constants are valid for the entire lifetime of the program within the scope they are declared in. They are useful for values like configuration parameters, mathematical constants, and maximum limits that are shared across your codebase.
const MAX_CONNECTIONS: u32 = 10_000;
const PI: f64 = 3.141_592_653_589_793;
const GREETING: &str = "Welcome to Rust";
fn circle_area(radius: f64) -> f64 {
PI * radius * radius
}
fn main() {
println!("{}", GREETING);
println!("Max connections allowed: {}", MAX_CONNECTIONS);
println!("Area of circle (r=5): {:.2}", circle_area(5.0));
// Constants can be used in any scope
const LOCAL_LIMIT: i32 = 100;
for i in 0..3 {
println!("Limit: {}, iteration: {}", LOCAL_LIMIT, i);
}
}