02. Variables and Mutability
📋 Jump to Takeaways🎁 What if every variable you created was immutable by default, you literally couldn't change it after assignment? Why would a language force that constraint on you?
Rust makes immutability the default because mutable state is the root of most bugs in concurrent programs. You opt in to mutation explicitly, making your intent clear to both the compiler and anyone reading your code.
Immutable Bindings with let
When you declare a variable with let, it cannot be reassigned:
fn main() {
let x = 5;
println!("x is: {}", x);
// x = 10; // ERROR: cannot assign twice to immutable variable
}This isn't a limitation, it's a guarantee. When you see let x = 5, you know x is 5 for its entire lifetime. No hunting through code to find where it changed.
Mutable Bindings with let mut
When you need a variable to change, add mut:
fn main() {
let mut counter = 0;
println!("counter: {}", counter); // counter: 0
counter += 1;
println!("counter: {}", counter); // counter: 1
counter += 1;
println!("counter: {}", counter); // counter: 2
}The mut keyword is a signal. It tells readers: "this value will change." Every mutation point in your program is now visible at the declaration site.
Shadowing
You can redeclare a variable with the same name using a new let. This is called shadowing, the new binding hides the previous one:
fn main() {
let x = 5;
println!("x: {}", x); // x: 5
let x = x + 10;
println!("x: {}", x); // x: 15
let x = x * 2;
println!("x: {}", x); // x: 30
}Each let x creates a brand new variable. The old one still exists in memory but is no longer accessible by that name.
Shadowing differs from mut because you can change the type:
fn main() {
let spaces = " "; // &str
let spaces = spaces.len(); // usize — different type, same name
println!("spaces: {}", spaces); // spaces: 3
}With mut, changing the type would be a compile error. Shadowing lets you transform a value and reuse a meaningful name.
Type Inference
Rust's compiler infers types from context. You don't always need annotations:
fn main() {
let x = 42; // inferred as i32
let y = 3.14; // inferred as f64
let active = true; // inferred as bool
let name = "Rust"; // inferred as &str
println!("{} {} {} {}", x, y, active, name);
// Output: 42 3.14 true Rust
}The compiler chooses i32 for integers and f64 for floats when there's no other context. Inference keeps code concise without sacrificing type safety.
Explicit Type Annotations
When inference isn't enough or you want a specific type, annotate with a colon:
fn main() {
let small: i8 = 127; // 8-bit signed
let big: i64 = 9_000_000_000; // 64-bit signed
let precise: f32 = 3.14; // 32-bit float (not the default f64)
let byte: u8 = 255; // 8-bit unsigned
println!("small: {}", small); // small: 127
println!("big: {}", big); // big: 9000000000
println!("precise: {}", precise); // precise: 3.14
println!("byte: {}", byte); // byte: 255
}Annotations are required when the compiler can't determine the type, for example, when parsing a string into a number.
const vs let
const values are compile-time constants. They differ from let in important ways:
const MAX_SCORE: u32 = 100;
const PI: f64 = 3.14159265358979;
fn main() {
let current_score: u32 = 85;
println!("Score: {} / {}", current_score, MAX_SCORE);
// Output: Score: 85 / 100
println!("Pi: {}", PI);
// Output: Pi: 3.14159265358979
}Key differences:
constrequires a type annotation, alwaysconstmust be a compile-time computable value, no function callsconstcan be declared in any scope, including globalconstis inlined everywhere it's used, no memory address
Use const for values that are truly fixed for the entire program. Use let for everything else.
Key Takeaways
letcreates immutable bindings, reassignment is a compile errorlet mutexplicitly opts into mutation- Shadowing (
let x = ...again) creates a new variable and can change the type - Rust infers types but you can annotate with
: Type - The compiler defaults to
i32for integers andf64for floats constis compile-time, requires a type annotation, and can live globally
🎁 You know how to name and mutate values. But what kinds of values can you hold? Next up: the type system itself, every integer size, why char is 4 bytes, and the difference between a String you own and a &str you borrow.