← all lessons
Foundations · #2 of 13

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:

  1. The number 30 is created and stored somewhere in memory.
  2. 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).
  3. The name age is 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 exampleType name in RustWhat it is
42i32A 32-bit signed integer
3.14f64A 64-bit floating-point number (decimals)
"hello"&strA piece of text
true, falseboolA yes/no value
'A'charA 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:

Reassigning an immutable variable fails to compile editable · real rustc
Open in Playground ↗ ready

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):

Opt into change with mut editable · real rustc
Open in Playground ↗ ready

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.

Portrait of Robin Milner
Robin Milner · 1934–2010 Created ML (1973), the language whose immutable-by-default tradition Rust inherits, and co-discovered the type-inference algorithm that lets Rust guess your types correctly with zero runtime cost. Turing Award, 1991.

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:

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:

Shadowing a String into a number editable · real rustc
Open in Playground ↗ ready

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:

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:

A tuple and an array editable · real rustc
Open in Playground ↗ ready

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:

Overflow panics in debug mode editable · real rustc
Open in Playground ↗ ready

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

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.