Variables, Mutability & Scalar/Compound Types
In Rust, every variable is locked by default — you must ask permission to change it, and that rule alone prevents a whole category of bugs.
In most languages, a variable you declare is mutable unless you go out of your way to mark it otherwise — final in Java, const in JavaScript when you remember to type it. Rust flips that default: writing let x = 5; creates a variable you cannot reassign, and the compiler will stop you with a clear error — cannot assign twice to immutable variable — the moment you try. That's not an arbitrary restriction; it's a deliberate design choice, because most variables in real programs are never reassigned, and making immutability the default means every mut you do see in someone's code is a signal: "this value changes, pay attention here."
When a value genuinely needs to change, you opt in with let mut x = 5;, and after that x = 6; is perfectly legal. The type of a mut variable can never change across a reassignment, though — you're updating the same binding in place, not creating a new one. This distinction matters more than it looks, because Rust gives you a second, completely different tool for a similar-looking situation: shadowing.
Shadowing means declaring a new variable with let using a name that already exists in scope. It looks like reassignment, but it isn't — each let creates a brand-new binding, which is why you can shadow a &str with a usize, or an immutable value with another immutable value that happens to be a transformed version of the first. This is idiomatic Rust for a common pattern: parsing or transforming a value through a few steps without needing a mut variable or a pile of differently-named variables like raw_input, trimmed_input, parsed_input.
Underneath all of this sit Rust's scalar types — the ones that represent a single value. Integers come in signed (i8 through i128, plus isize) and unsigned (u8 through u128, plus usize) flavors, where the number is the bit width; if you don't annotate one, Rust defaults to i32, which is the fastest choice on most machines. usize and isize are special: their size matches your platform's pointer width, and usize is what indexes into arrays and collections. Floating-point numbers come as f32 or f64, defaulting to f64 for its extra precision at essentially no cost on modern CPUs. Rounding out the set are bool (true/false) and char, which — unlike C's single-byte char — is always a full 4-byte Unicode scalar value, so it can hold an emoji or an accented letter just as easily as 'a'.
Compound types group multiple values into one. A tuple, written (3.0, 4.0, 0.0), can mix different types in fixed positions, and you can pull values out either by destructuring (let (x, y, z) = point;) or by index (point.0). An array, written [i32; 3], is fixed in length and holds only one type, laid out contiguously on the stack — which is exactly why it can't grow; when you need a resizable list you'll reach for Vec<T> in a later lesson, which is Rust's heap-allocated, growable cousin of the array.
const looks similar to let but is a different mechanism entirely: a constant must always carry an explicit type annotation, its value must be computable entirely at compile time, it can never be shadowed or made mutable, and by convention its name is SCREAMING_SNAKE_CASE. Because it has no fixed memory address the way a static does, the compiler is free to inline its value everywhere it's used — it behaves less like a variable and more like a named, type-checked literal that happens to live in one place in your source.
Finally, notice how little type annotation you've actually had to write. Rust's compiler infers the type of most let bindings from how the value is used later in the function, which is why let x = 5; and let point: (f64, f64, f64) = (3.0, 4.0, 0.0) can coexist — the tuple needed an annotation only because floating-point literals default to f64 regardless, so annotating documents intent rather than resolving ambiguity. Inference stops at function boundaries, though: every function parameter and return type must be written out explicitly, because Rust deliberately doesn't infer across a public API boundary — a caller reading just the signature should never need to look inside the function body to know what types it expects.
fn main() {let x = 5;println!("The value of x is: {}", x);let mut y = 5;y = 6;println!("The value of y is: {}", y);}
let x is immutable and cannot be reassigned, but let mut y opts into reassignment — removing mut from y's declaration would turn the y = 6 line into a compile error.
fn main() {let spaces = " ";let spaces = spaces.len();println!("Number of spaces: {}", spaces);let x = 5;let x = x + 1;let x = x * 2;println!("x is now: {}", x);}
Shadowing lets the name spaces go from a &str to a usize, and lets x be transformed twice in a row, without ever needing a mut binding.
const MAX_PLAYERS: u32 = 4;fn main() {let point: (f64, f64, f64) = (3.0, 4.0, 0.0);let (x, y, z) = point;println!("Coordinates: ({}, {}, {})", x, y, z);let scores: [i32; 3] = [90, 85, 100];println!("First score: {}", scores[0]);println!("Max players allowed: {}", MAX_PLAYERS);}
A tuple destructured by pattern, a fixed-size array indexed by position, and a top-level const — notice const requires its type annotation while the let bindings do not.
🧠 Check your understanding
0/1 · 0/1 answered1. What's the real difference between shadowing a variable with a new let x = ... and reassigning it with x = ... after declaring let mut x?