Variables, Types & Mutability
Naming data, knowing what shape it is, and saying when it's allowed to change
Most languages let a variable change whenever you feel like it. Rust makes you ask first — and in return, it promises that anything you didn’t mark as changeable will hold still while you read it.
It’s a small inversion of an old habit. It deletes a surprising number of bugs.
Almost every line of Rust does one of three things: makes a new variable, reads one, or changes one. Rust layers two ideas on top that many languages leave out: a variable can’t change unless you explicitly say so, and every value has a known type — a shape — the compiler tracks for you. Both remove whole classes of bugs in exchange for a few extra keystrokes up front.
What is a variable, really?
A variable is a name attached to a piece of data. When you write:
let age = 30;
three things happen at once:
- The number
30is created and stored somewhere in memory. - The compiler looks at the right-hand side (
30) and figures out its type — here, a 32-bit integer (a whole number that fits in 32 bits of space). - The name
ageis attached to that data, with that type.
From now on, anywhere you write age in this scope, the compiler reads it as “the integer stored there.”
What is a type?
A type is the shape of a value — what it is and what you can do with it. Numbers, text, true/false values, and lists are all different types, each with its own set of operations.
| Value example | Type name in Rust | What it is |
|---|---|---|
42 | i32 | A 32-bit signed integer |
3.14 | f64 | A 64-bit floating-point number (decimals) |
"hello" | &str | A piece of text |
true, false | bool | A yes/no value |
'A' | char | A single character |
The compiler tracks the type of every value. Try something nonsensical — adding a number to a piece of text — and it refuses to build the program and points at the exact line. That error appears at compile time; the bug never reaches a running program.
Immutability is the default
In most languages, a variable can be reassigned freely:
x = 10
x = 20 # fine
Rust says no by default. Once you bind a name to a value, the name cannot be reassigned unless you opt in. Hit Run below and read the error — this is the single most common message a new Rust programmer meets:
The compiler reports error[E0384]: cannot assign twice to immutable variable \n`, and — this is the part worth noticing — it suggests the fix: consider making this binding mutable. To allow changes, write let mut (mut` is short for mutable):
This default — immutable unless you ask — is unusual and powerful. When you read someone else’s Rust (or your own a month later), a variable without mut is a promise it never changes underneath you. A whole genre of “wait, when did this get modified?” bugs simply disappears.
Immutability-by-default didn’t start with Rust. It comes from the functional-language tradition — languages like ML and Haskell — where values that never change are the norm and mutation is the exception you reach for deliberately. Rust borrowed that stance and inverted the C/Java habit, where everything is changeable until you bolt on a keyword like const or final to stop it.
Type inference: the compiler usually figures it out
You can always write the type by hand:
let a: i32 = 42;
let b: f64 = 3.14;
let c: bool = true;
But Rust can usually work it out from context, and you’ll see this far more often:
let a = 42; // inferred i32 (default integer type)
let b = 3.14; // inferred f64 (default float type)
let c = true; // inferred bool
That guessing isn’t magic, and it isn’t a runtime cost — the type is nailed down at compile time, then the annotation is simply absent from your source. Inference only fails when context isn’t enough to decide. For example, when a parser could produce several integer types and nothing pins down which:
let n: u32 = "42".parse().unwrap(); // u32 needed; parse() accepts many integer types
Don’t worry about .parse() or .unwrap() yet — they return in lesson 10. The point is just: sometimes you write the type, usually you don’t.
The integer zoo
Rust has not one integer type but many. The choice matters when you care about size or sign:
- Signed integers (can be negative):
i8,i16,i32,i64,i128— the number is how many bits the value occupies.i32is the default when you don’t specify. - Unsigned integers (zero or positive only):
u8,u16,u32,u64,u128— same idea, no negatives. isizeandusize: integers sized to the computer’s natural pointer width (32 bits on a 32-bit machine, 64 on a 64-bit one).usizeis what you use to index into lists.
Pick the smallest type that fits the values you’ll actually hold. If unsure: i32 for general arithmetic, usize for indexing.
Shadowing: reusing a name
Sometimes you want to keep a name but change what type it holds. Rust allows this through shadowing — a fresh let with the same name. The old binding is hidden from that point on. Run this:
It prints count + 1 = 43. Each let creates a brand-new variable that happens to reuse the name; the old one is forgotten. This is not mutation — you never changed a value in place, you declared a second variable. That’s why the type is allowed to change from &str to u32, which let mut would not permit. People reach for shadowing constantly to convert between types (text to number is the classic) without inventing clumsy names like count_text and count_num.
Compound types: tuples and arrays
So far, one name holds one value. Two built-in compound types let one name hold several:
- A tuple groups a fixed number of values of possibly different types:
(3, 7, 'X'). - An array holds a fixed number of values of the same type:
[1, 2, 3].
Both have a length fixed at compile time. You pull values back out by destructuring a tuple into names, or indexing either one. Run this:
It prints at 3,7 draw X; y again = 7 then days tracked: 7. Notice point.1 reaches the second tuple field by position, and [0; 7] is shorthand for “seven copies of 0.” For lists that grow, you’ll want a Vec — but that lives on the heap and is a later lesson. Tuples and arrays are fixed-size and cheap.
A bug worth knowing about: integer overflow
What happens when you add 1 to the biggest possible u8? A u8 holds 0 through 255. The mathematically correct answer is 256 — which doesn’t fit. This is integer overflow: the result is too big for the type. Watch Rust handle it. This loop counts a u8 past its ceiling:
Hit Run. You’ll see 251, 252, 253, 254, 255 print, and then — instead of a wrong answer — the program panics: thread 'main' panicked at ... attempt to add with overflow. A panic is Rust’s emergency stop: it halts the program at the offending line rather than letting a corrupt value continue. (You’ll meet panics throughout this course.) This is the debug build behaviour, the default in the playground.
In a release build — compiled with optimisations for speed — the same code does not panic. The value silently wraps around: 255 + 1 becomes 0, like a car odometer rolling over, and the loop happily prints 255, 0, 1, 2, .... Why the split? Debug builds prioritise catching bugs; release builds prioritise speed, and checking every addition for overflow costs CPU cycles that add up in a tight loop.
If you want one specific behaviour regardless of build mode, integers carry explicit methods for it:
let a = x.checked_add(1); // returns Option: Some(v), or None on overflow
let b = x.wrapping_add(1); // always wraps (0 on overflow)
let c = x.saturating_add(1); // clamps at the max (255 on overflow)
Real-world code reaches for these constantly, because “I know exactly what should happen at the boundary” is a better stance than “I hope it never overflows.”
Why a 'type' is more than a label
It’s tempting to think of a type as just a tag that says “this is a number.” In a system like Rust’s, it’s closer to a proof obligation. Every value carries, at compile time, a set of operations it’s allowed to participate in, and the compiler refuses any expression that violates that set — adding an &str to an i32, indexing an array with a float, assigning a u32 where an i64 is required. None of these checks exist when the program runs; they’re resolved entirely before the binary is produced, which is why they cost nothing at runtime. This is the same lineage as the Hindley–Milner inference above: a type system rigorous enough that the compiler can reason about your program, catch contradictions, and still reconstruct the types you left off. The friction you feel writing let n: u32 once in a while is the price of a machine double-checking your arithmetic before a single instruction executes.
Key takeaways
- A variable is a name attached to data; a type is the shape that says what the data is and what you can do with it.
- Variables are immutable by default — a tradition borrowed from functional languages. Opt into change with
let mut. - Rust usually infers types for you (a descendant of Hindley–Milner inference), at zero runtime cost. Write the type when context isn’t enough.
- Integers come in many widths; pick
i32if unsure,usizefor indexing. - Shadowing (a fresh
letreusing a name) switches a variable’s type without renaming — and is not mutation. - Tuples group different types; arrays hold one type. Both are fixed-size.
- Integer overflow panics in debug, wraps in release. Use
checked_/wrapping_/saturating_when the boundary behaviour matters.
You now have the vocabulary of every Rust value: a name, a shape, and a clear answer to whether it’s allowed to change. Hold that last idea — who may change what — because it’s about to grow up.
The next lessons teach control flow, and then ownership: the rule that decides not just whether a value can change, but who is even allowed to touch it.