Basic

Variables and Immutability

Declare variables with let, understand immutability, and shadowing.

What you will see

5

About 2 min of reading

Quick check

Before this new lesson, one small question about the lesson you just finished. Recall it, do not look it up.

Try It

You will bind values with let, try to reassign an immutable variable, use mut, and shadow a name.

1

I bind a string, an integer, and a float, then print them together. Rust infers each type from the literal.

fn main() { let name = "Ada"; let age = 30; let height = 1.75; println!("{} {} {}", name, age, height); }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
Ada 30 1.75
2

I declare a mutable counter, add one, and print it. Without the mut keyword this reassignment would fail to compile.

fn main() { let mut count = 0; count += 1; println!("{}", count); }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
1
3

I shadow a name with a new let binding that holds a different value. The second value is the one used afterward.

fn main() { let spaces = 5; let spaces = spaces + 3; println!("{}", spaces); }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
8
Shadowing replaces the previous binding in that scope.
4

I try to reassign an immutable variable. The compiler rejects the assignment, showing Rust defaults to immutability.

fn main() { let x = 5; x = 6; println!("{}", x); }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
COMPILE_ERROR
A let binding is immutable unless you write let mut.
5

I annotate an explicit type and print it. Annotating types helps when inference cannot choose the type on its own.

fn main() { let x: i32 = 5; println!("{}", x); }

Before you run it, say out loud what it will print. Then open the answer.

What it prints
5

Put it in order

Before you write anything, put these lines back in the right order. Click a line to move it up one step. This support step is easier than writing code from scratch, and it is here to make the last example stick.

Data Flow

A value is bound to an immutable name; mut or shadowing allows later change.
1 Write a literal on the right of let
↓
2 The compiler infers the type
↓
3 A binding is created
↓
4 Reassign only if mut was used or shadowed
Why this works
Immutability by default

Rust defaults to immutable bindings:

let x = 5;
x = 6;   // compile error: cannot assign twice

This forces you to think about where state changes. Code that cannot be mutated is easier to reason about and safe to share.

Type inference

Rust infers types from context:

let name = "Ada";   // inferred as &str
let age = 30;       // inferred as i32
let pi = 3.14;      // inferred as f64

If inference fails, you can annotate explicitly:

let x: f64 = 3.14;

The default integer type is i32 and floats are f64.

Examples

Declaring variables

Declaring variables

let name = "Ada";
let age = 30;
let height = 1.75;

let binds a name to a value. Rust infers the type from the value. By default, variables are **immutable*: you cannot reassign them.

Why it works: immutability by default makes programs safer; the compiler catches accidental changes.

Making variables mutable

The mut keyword

let mut count = 0;
count += 1;   // allowed because of mut

let mut makes a variable reassignable. Without it, count = 5 is a compile error.

Why it works: Rust only allows mutation where you explicitly opt in with mut.

Shadowing

Reusing a name

let spaces = 5;
let spaces = spaces + 3;   // shadowing

Shadowing lets a new let reuse the same name with a different type or value. The old binding is replaced in that scope.

Why it works: shadowing is handy for converting types while keeping readable names.

Words this lesson uses

Each one is defined here on this site. Open the entry for the full definition and the other words it points at.

← Your First Rust Program
Previous Lesson
Your First Rust Program
Next Lesson
Primitive Types
Primitive Types →