Lesson 2 of 22·13 min

Variables and Mutability

Variables in Rust look familiar at first: let x = 5;. The difference is the default. A Rust variable cannot change unless you say so, and the compiler holds you to it. This lesson covers declaring variables, making them mutable, shadowing a name with a new value, and the const items you use for values that are fixed for the whole program.

Declaring variables with let

let binds a value to a name:

fn main() {
    let planet = "Mars";
    let moons = 2;
    println!("{planet} has {moons} moons");
}
Mars has 2 moons

You didn't write any types, but every variable still has one. The compiler infers them from the values: planet is a string slice (&str) and moons is an integer (i32, the default integer type). When you want a specific type, or when the compiler can't work it out, add an annotation after the name:

fn main() {
    let moons: u32 = 2;
    let gravity: f64 = 3.72;
    let habitable: bool = false;
    println!("{moons} {gravity} {habitable}");
}
2 3.72 false

u32 is an unsigned 32-bit integer, f64 a 64-bit float and bool a boolean. The next lesson goes through the primitive types in detail.

Variable names use snake_case: lowercase words separated by underscores, like max_speed or user_count. The compiler warns you if you use another style.

Immutable by default

Once a variable has a value, that value can't be changed:

fn main() {
    let score = 10;
    println!("score: {score}");
    score = 20;
    println!("score: {score}");
}

This is deliberate. When you read let score = 10;, you know score is 10 for as long as it exists, without checking every line below it. Most values in a program never need to change, so Rust makes that the default and makes change the thing you have to ask for. It also matters later: the borrow checker, which lets Rust share data safely between parts of a program, relies on knowing what can change and what can't.

Opting into change with mut

Put mut after let to make a variable mutable:

fn main() {
    let mut score = 10;
    println!("score: {score}");
    score = 20;
    score += 5;
    println!("score: {score}");
}
score: 10
score: 25

+= (and -=, *=, /=) update a mutable variable in place.

mut is needed whenever the value itself is modified, not only for =. Calling a method that changes a value, such as push_str on a String, needs a mutable variable too:

fn main() {
    let mut greeting = String::from("Hello");
    greeting.push_str(", Ferris");
    greeting.push('!');
    println!("{greeting}");
}
Hello, Ferris!

String is Rust's growable, owned string type; String::from makes one from a string literal. You will meet it properly in the ownership lesson.

The opposite also holds: if you mark a variable mut and never change it, the compiler warns you, because the mut is now misleading:

warning: variable does not need to be mutable
 --> src/main.rs:2:9
  |
2 |     let mut lives = 3;
  |         ----^^^^^
  |         |
  |         help: remove this `mut`

Declaring now, assigning later

A let doesn't have to give a value straight away. You can declare a variable and assign it exactly once later, as long as the compiler can see that every path assigns it before it is read:

fn main() {
    let hour = 14;
    let greeting;
    if hour < 12 {
        greeting = "Good morning";
    } else {
        greeting = "Good afternoon";
    }
    println!("{greeting}");
}
Good afternoon

greeting is not mut: it is assigned once on each path. In practice you will more often write this with if as an expression, which the control flow lesson covers.

Shadowing

You can declare a new variable with the same name as an existing one. The new one shadows the old: from that line on, the name refers to the new value.

fn main() {
    let temperature = 21;
    let temperature = temperature * 9 / 5 + 32;
    println!("{temperature}°F");
 
    let input = "  42  ";
    let input = input.trim();
    let input: i32 = input.parse().unwrap();
    println!("{}", input + 1);
}
69°F
43

Each let creates a brand new variable; nothing is mutated. That gives shadowing two uses that mut doesn't have:

  • Transforming a value step by step while keeping the name, and leaving the result immutable. After the second line, temperature can't be changed by accident.
  • Changing the type. input starts as a string with spaces, becomes a trimmed string, then becomes an i32. With mut you would need three names, such as input, trimmed and number. (parse and unwrap are covered in the lesson on Option and Result.)

Shadowing also follows scope. A variable declared inside a block { ... } only lives until the end of that block, and the outer one comes back afterwards:

fn main() {
    let level = 1;
    {
        let level = level + 10;
        println!("inner level: {level}");
    }
    println!("outer level: {level}");
}
inner level: 11
outer level: 1

Shadowing or mut?

Use mut when a value really changes over time: a counter, a running total, a buffer you keep appending to. Use shadowing when you are deriving a new value from an old one and the old one is no longer interesting.

Constants

A constant is a value that is fixed for the whole program. Declare it with const:

const SECONDS_PER_MINUTE: u32 = 60;
const SECONDS_PER_HOUR: u32 = 60 * SECONDS_PER_MINUTE;
 
fn main() {
    const GREETING: &str = "Hi";
    let hours = 3;
    let seconds = hours * SECONDS_PER_HOUR;
    println!("{GREETING}! {hours} hours is {seconds} seconds");
}
Hi! 3 hours is 10800 seconds

Constants differ from let variables in a few ways:

  • The type is always written out. There is no inference for const.
  • They can never be mutable. There is no const mut.
  • The value must be known at compile time. Literals, arithmetic on other constants and a few other things are fine; a value computed while the program runs is not.
  • They can live anywhere, including outside any function. Constants at the top of a file are visible to every function in it, which is where most of them go. A constant declared inside a function, like GREETING, is only visible there.
  • They are named in SCREAMING_SNAKE_CASE.

Add pub in front (pub const ...) to make a constant usable from other modules, just like a function.

The compiler copies a constant's value into every place it is used, so a constant has no single address in memory. When you need one global value that lives at a fixed location, Rust has static, which you will rarely need at this stage.

Common mistakes

Assigning to a variable without mut

You saw this one above: cannot assign twice to immutable variable (E0384). Either add mut, or, if you are deriving a new value, shadow the name with another let.

Changing the type of a mut variable

mut lets the value change, but never the type. This tries to store a number in a variable that holds a string:

fn main() {
    let mut input = "  42  ";
    input = input.trim().len();
    println!("{input}");
}

The fix is shadowing: let input = input.trim().len(); creates a new variable with the new type.

Reading a variable before it has a value

A variable declared without a value can't be read until it is assigned:

fn main() {
    let total: i32;
    println!("{total}");
}

Rust has no default "zero" or "null" value for variables. Give the variable a value, or make sure every path assigns one before it is used.

Leaving the type off a constant

const MAX_PLAYERS = 4;
 
fn main() {
    println!("{MAX_PLAYERS}");
}

Constants always need an annotation: const MAX_PLAYERS: u32 = 4;.

Using a runtime value in a constant

A const can't be built from a let variable, because the variable's value only exists once the program runs:

error[E0435]: attempt to use a non-constant value in a constant
 --> src/main.rs:3:30
  |
3 |     const MAX_PLAYERS: u32 = players * 2;
  |                              ^^^^^^^ non-constant value

If the value depends on something computed at runtime, it is a let variable, not a constant.

Unused variables

A variable you never read triggers a warning. If you really do want to keep it, for example while a function is half written, start its name with an underscore:

fn main() {
    let _unused = 5;
}

Summary

  • let declares a variable. Types are inferred, or written as let x: u32.
  • Variables are immutable by default. Add mut to change the value, including through methods like push_str.
  • A variable can be declared first and assigned later, once on every path, before it is read.
  • Shadowing (let x = ... again) creates a new variable with the same name. It can change the type and keeps the result immutable; it ends with the block it is declared in.
  • const values are always typed, never mutable, known at compile time and named in SCREAMING_SNAKE_CASE. They can be declared at the top level or inside a function.

Practise what you read

Challenges that use the ideas from this lesson.