Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Rust (2024 edition)  Module 2 – Values, types, functions and control flow

Variables, mutability, shadowing and constants

Bind values with let and mut, reuse a name by shadowing, write a type where Rust cannot infer it, and choose between let, const and static.

  • Beginner
  • 20 minutes
  • Examples run with Rust 1.99.0 and Python 3.14.8
  • By MySmartCoPilot
Free lesson, Pro answers: 20 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

What you will learn

  • Declare immutable and mutable bindings, and fix error E0384 when a binding is assigned twice
  • Use shadowing to turn a value into a new one, of a new type if needed, and tell it apart from mut
  • Annotate a type where inference cannot decide, such as the result of parse
  • Choose between let, const and static, and explain why Rust 2024 refuses references to a static mut
  • Predict when a binding is dropped, and what a name starting with _ and a bare _ change

Before you start

What you’ll learn · Before you start

What you will learn

  • Declare immutable and mutable bindings, and fix error E0384 when a binding is assigned twice
  • Use shadowing to turn a value into a new one, of a new type if needed, and tell it apart from mut
  • Annotate a type where inference cannot decide, such as the result of parse
  • Choose between let, const and static, and explain why Rust 2024 refuses references to a static mut
  • Predict when a binding is dropped, and what a name starting with _ and a bare _ change

Before you start

On this page

Add up three groups of passengers at a ticket gate with sold = sold + group;, and rustc refuses the eight-line program: error E0384, cannot assign twice to immutable variable. In Rust a name keeps the first value it is given unless you ask for it to change, and the compiler holds you to that.

This lesson covers each way a name and a value can be tied together: a binding: A name tied to a value by let or by a pattern. It is immutable unless it is declared with mut, and it lasts until the end of the block that made it. that never changes, one that may (mut), a new binding under an old name, and values fixed for the whole program (const and static). Each comes with the error or warning rustc 1.99.0 prints when it is misused, and the lesson ends with the moment each value is drop: What happens to a value when the binding that owns it goes out of scope: its clean-up runs and any memory it owns is freed. A block's bindings are dropped in the reverse order they were made..

A let binding keeps its first value unless you write mut

Here is the gate’s counter: it starts at 0 and adds three groups of passengers, 2, 3 and 1. The compiler refuses it before any program exists:

sold.rs: a total that may not change Rust · sold.rs
// Counting the tickets sold to three groups at a gate.
fn main() {
    let sold = 0;
    for group in [2, 3, 1] {
        sold = sold + group;
    }
    println!("tickets sold: {sold}");
}

Output (exit status 1)

Printed as an error (standard error)

error[E0384]: cannot assign twice to immutable variable `sold`
 --> sold.rs:5:9
  |
3 |     let sold = 0;
  |         ---- first assignment to `sold`
4 |     for group in [2, 3, 1] {
5 |         sold = sold + group;
  |         ^^^^^^^^^^^^^^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
3 |     let mut sold = 0;
  |         +++

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0384`.

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O sold.rs && ./sold

The ^^^^^ line marks what rustc refuses, the assignment on line 5, and the ---- label says where sold got its first and only value, on line 3. A name bound with let is immutable: it keeps that value until it goes away. The help line names the fix, and with it the program builds and counts 6:

sold_mut.rs: the same count, declared mut Rust · sold_mut.rs
// The same count, with a binding that may change.
fn main() {
    let mut sold = 0;
    for group in [2, 3, 1] {
        sold = sold + group;
    }
    println!("tickets sold: {sold}");
}

Output

tickets sold: 6

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O sold_mut.rs && ./sold_mut

let mut makes the binding mutable: an assignment may give it a new value, as often as you like, but always a value of the same type. Everything else stays as it was, so the word mut is the whole difference between the two programs.

Immutable is the default because many values in a program never change once they are worked out, and a reader who sees let without mut knows the name means one value until its block ends. The compiler checks that promise on every line, so you never have to look for an assignment hidden further down. It checks the other direction too: a mut that nothing ever uses gets a warning, which the session in the last concept section shows.

Adding mut everywhere to make errors go away

let mut on every binding compiles, but it throws away the promise that a value stays put, and rustc then warns about every mut that changes nothing. Write mut only where the value really changes, such as the running total in sold_mut.rs.

Checkpoint

Check: Immutable bindings and mut

  1. Question 1 of 2 A program has let fare = 25; and, two lines later, fare = 30;. What happens when you build it?

    Choose one answer.

    A hint

    Look at how fare was declared.

    Show the answer: Question 1 of 2

    Answer: rustc refuses it with E0384, cannot assign twice to immutable variable

    A let binding without mut is immutable. rustc refuses the second assignment with E0384 and suggests the fix: let mut fare = 25;, after which fare may take new values of the same type.

  2. Question 2 of 2 Why are let bindings immutable unless you write mut?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: So that a reader and the compiler know a value never changes after its let, and the compiler checks it

    Immutability by default means a let without mut is a promise that the value stays the same until the end of its block. The compiler checks the promise, and it also warns about a mut that is never used.

Shadowing makes a new binding under the same name

A program that asks its user for a number meets a naming problem at once. The answer arrives as a line of text, its spaces and newline have to go, and then it has to become a number: three values with one meaning. Rust lets all three be called tickets. This program reads the line 4 from its standard input:

tickets.rs: one name for the line, the text and the number Rust · tickets.rs
// One name for the line a user typed, the text in it and the number it means.
use std::io;

const TICKET_PAISE: u32 = 2_500;

fn main() {
    let mut tickets = String::new();
    io::stdin().read_line(&mut tickets).expect("no line to read");
    println!("the line:   {tickets:?}");

    let tickets = tickets.trim();
    println!("the text:   {tickets:?}");

    let tickets: u32 = tickets.parse().expect("not a whole number");
    println!("the number: {tickets}, fare {} paise", tickets * TICKET_PAISE);
}

Input (standard input)

  4

Output

the line:   "  4\n"
the text:   "4"
the number: 4, fare 10000 paise

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O tickets.rs && ./tickets

Each let tickets makes a new binding that hides the one before it from that line on. That is shadowing: Declaring a new binding with the name of an existing one. From that line on the name refers to the new binding, which may have another type, while the old value stays as it was until its block ends.. The new binding may have a new type: a String, then a &str, then a u32. And the later bindings are immutable, because they were declared without mut, while the first one had to be mutable for read_line to fill it. Step through the three bindings and watch what happens to the older values:

Interactive · step by step Three bindings named tickets in tickets.rs Step through main one let statement at a time. Watch which binding answers to the name, and which values are still there.
Three bindings named tickets in tickets.rs, step 6 of 6. Showing tickets: String (hidden by the new name), The line in memory (freed with its owner), tickets: &str (hidden by the new name), tickets: u32, The fare (10000 paise).ownspoints into ittimes 2,500tickets: Stringhidden by the new nameThe line in memoryfreed with its ownertickets: &strhidden by the new nametickets: u32the number 4The fare10000 paise

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. The first binding. let mut tickets = String::new(); makes an empty String, and read_line appends the line the user typed: two spaces, a 4 and a newline. This first tickets owns that text.
  2. A second tickets. let tickets = tickets.trim(); is a new binding with the same name. Its value is a &str that points at the 4 inside the same line: trimming copies nothing.
  3. The old value stays. The first tickets can no longer be named, but it still owns the line, so the text the new binding points into stays valid. Shadowing hid a name; it dropped nothing.
  4. A third tickets. let tickets: u32 = tickets.parse()… makes a third binding, a number. The annotation : u32 tells parse which type to produce, and this binding is not mut, so it can never change.
  5. The newest one answers. From here on, tickets means the number. tickets * TICKET_PAISE reads the u32 4 and gives 10000 paise, the fare the program prints.
  6. All three are dropped. At the end of main the three bindings are dropped, the newest first: the number, then the &str, then the String, which frees the line. Each binding lived until the end of the block that made it.

The older values are still there: the trimmed text points into the line, which the first tickets still owns. Shadowing changes which value a name refers to; it never changes or drops a value. Try the same conversion with one mutable binding instead, and the type gets in the way:

retype.rs: mut cannot change a type Rust · retype.rs
// mut lets a value change, but never its type.
fn main() {
    let mut tickets = "  4";
    tickets = tickets.trim();
    tickets = tickets.parse::<u32>().expect("not a whole number");
    println!("{tickets}");
}

Output (exit status 1)

Printed as an error (standard error)

error[E0308]: mismatched types
 --> retype.rs:5:15
  |
3 |     let mut tickets = "  4";
  |                       ----- expected due to this value
4 |     tickets = tickets.trim();
5 |     tickets = tickets.parse::<u32>().expect("not a whole number");
  |               ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ expected `&str`, found `u32`

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0308`.

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O retype.rs && ./retype

tickets was a &str from its first line, so every value assigned to it must be a &str, and a u32 is refused with E0308, mismatched types. Choose between the two by what the code does:

let mut or shadowing?
Criterion let mutShadowing: let again
What changes The value inside the same bindingWhich binding the name refers to
A new type Refused (E0308)Allowed
The old value ReplacedKept until its block ends, out of reach
Later changes AllowedOnly if the new binding is mut
When to choose A running total, a counter, a buffer you fillSteps of a conversion: line, text, number

Key idea

mut lets one binding take new values of the same type; shadowing makes a new binding, of any type, and leaves the old value alone.

Checkpoint

Check: Shadowing a name

  1. Question 1 of 2 let n = "7"; then let n: u32 = n.parse().unwrap(); then let n = n * 2;. What is n at the end?

    Type a number.

    A hint

    Each let makes a new binding from the one before it.

    Show the answer: Question 1 of 2

    Answer: 14

    The first n is the text "7", the second the number 7 parsed from it, and the third a new binding worth 7 × 2 = 14. Each let shadows the one before; none of them changed a value in place.

  2. Question 2 of 2 let mut tickets = " 4"; then tickets = tickets.parse::<u32>().unwrap(); fails with E0308. Which change fixes it?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Write the second line as let tickets: u32 = tickets.trim().parse().unwrap();

    A mut binding keeps its type: tickets is a &str, so it cannot hold a u32. Shadowing with a new let makes a second binding that is a u32. The spaces must go too, because " 4".parse::<u32>() fails.

Rust works out each type, and asks when it cannot

Most bindings in this lesson have no type written, yet each has exactly one type, fixed when the program is compiled. rustc infers it from the value and from how the name is used afterwards. This program asks the standard library which types it chose:

inferred.rs: the types rustc chose Rust · inferred.rs
// No types written, except one: what did rustc choose?
use std::any::type_name_of_val;

fn main() {
    let tickets = 4;
    let price = 25.0;
    let fare: u64 = 2_500;
    let total = fare * 4;
    let typed = "7".parse::<u8>().expect("not a whole number");

    println!("tickets: {}", type_name_of_val(&tickets));
    println!("price:   {}", type_name_of_val(&price));
    println!("fare:    {}", type_name_of_val(&fare));
    println!("total:   {}", type_name_of_val(&total));
    println!("typed:   {}", type_name_of_val(&typed));
}

Output

tickets: i32
price:   f64
fare:    u64
total:   u64
typed:   u8

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O inferred.rs && ./inferred

tickets holds a whole number and nothing else in the program says which kind, so it gets the default for integers, i32; price gets f64, the default for numbers with a decimal point. total is a u64 because fare is one, and typed is a u8 because the call asked for it. (type_name_of_val is meant for messages like these; its exact text may change between compiler versions.)

When nothing can decide, the program does not compile. parse can turn text into dozens of types, and in this program nothing says which one is wanted:

untyped_parse.rs: parse with no type to aim for Rust · untyped_parse.rs
// parse can produce many types, and nothing here says which one.
fn main() {
    let text = "4";
    let tickets = text.parse().expect("not a whole number");
    println!("{tickets} tickets");
}

Output (exit status 1)

Printed as an error (standard error)

error[E0284]: type annotations needed
    --> untyped_parse.rs:4:9
     |
   4 |     let tickets = text.parse().expect("not a whole number");
     |         ^^^^^^^        ----- type must be known at this point
     |
     = note: cannot satisfy `<_ as FromStr>::Err == _`
     = note: the type must also implement `FromStr`
note: required by a bound in `core::str::<impl str>::parse`
    --> …/lib/rustlib/src/rust/library/core/src/str/mod.rs:2782:21
     |
2782 |     pub fn parse<F: FromStr>(&self) -> Result<F, F::Err> {
     |                     ^^^^^^^ required by this bound in `core::str::<impl str>::parse`
help: consider giving `tickets` an explicit type
     |
   4 |     let tickets: /* Type */ = text.parse().expect("not a whole number");
     |                ++++++++++++

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0284`.

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O untyped_parse.rs && ./untyped_parse

Read the first line and the help: E0284, type annotations needed, and a suggestion to give tickets an explicit type. The notes in between point into the source of the standard library, at the rule that parse needs a type it can produce. Either of two places can name it: on the binding, let tickets: u32 = text.parse()…, as tickets.rs does, or in the call, text.parse::<u32>(), the form Rust programmers call the turbofish.

Annotate the binding, not every binding

One annotation is usually enough: once tickets is a u32, every binding computed from it is inferred from it, as total was from fare. Write a type where a reader would otherwise have to work it out.

Checkpoint

Check: Inferred types and annotations

  1. Question 1 of 2 A program has let seats = 40;, and nothing else decides the type of seats. Which type does rustc give it?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: i32

    Without a suffix or a use that decides, an integer literal is an i32 and a decimal literal an f64, as inferred.rs printed for tickets and price.

  2. Question 2 of 2 let tickets = text.parse().expect("not a whole number"); fails with E0284, and tickets is only printed. Which changes make it build? Choose every one that works.

    Choose every answer that is right.

    Show the answer: Question 2 of 2

    Answer:

    • let tickets = text.parse::<u32>().expect("not a whole number");
    • let tickets: u32 = text.parse().expect("not a whole number");

    parse can produce many types. Name the one you want, on the binding or with ::<u32> on the call, or use the value in a way that decides it.

const and static hold values fixed for the whole program

Some values belong to the program itself rather than to one run of a function: a ticket price, a limit, the name of a gate. Rust gives them items of their own, which may sit outside every function:

consts.rs: two constants and a static Rust · consts.rs
// Values fixed for the whole program: two constants and a static.
const TICKET_PAISE: u32 = 2_500;
const DAY_PASS_PAISE: u32 = TICKET_PAISE * 8;
static GATE: &str = "Gate 3";

fn main() {
    const MAX_PER_SALE: u32 = 6; // a constant may live inside a function, too
    println!("{GATE}: ticket {TICKET_PAISE} paise, day pass {DAY_PASS_PAISE} paise");
    println!("at most {MAX_PER_SALE} tickets in one sale");
}

Output

Gate 3: ticket 2500 paise, day pass 20000 paise
at most 6 tickets in one sale

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O consts.rs && ./consts

A constant: A value declared with const and its type. The compiler works it out while it builds the program and copies it into every place that uses it, so a constant never changes. is computed while the program is built, TICKET_PAISE * 8 included, and copied into every place that uses it, so it has no single address. A static is a single value at one place in memory for the whole run: every use refers to that place, and it is never dropped. The Rust Reference advises a const unless you need that single address, a large amount of data or a value that can change inside. Both need their type written out, which the compiler enforces:

const_type.rs: a constant without its type Rust · const_type.rs
// A constant without its type.
const TICKET_PAISE = 2_500;

fn main() {
    println!("ticket: {TICKET_PAISE} paise");
}

Output (exit status 1)

Printed as an error (standard error)

error: missing type for `const` item
 --> const_type.rs:2:19
  |
2 | const TICKET_PAISE = 2_500;
  |                   ^ help: provide a type for the constant: `: i32`

error: aborting due to 1 previous error

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O const_type.rs && ./const_type

The convention is capitals with underscores for both, and a warning, non_upper_case_globals, says so when a name breaks it. A static mut, a static that may change, is where the 2024 edition draws a hard line. Reading or writing one already needed an unsafe block, and println! reads a value through a reference, which is now refused:

static_mut.rs: a mutable static in Rust 2024 Rust · static_mut.rs
// A static that may change, read the way println! reads a value.
static mut SOLD: u32 = 0;

fn main() {
    unsafe {
        SOLD += 4;
        println!("tickets sold: {SOLD}");
    }
}

Output (exit status 1)

Printed as an error (standard error)

error: creating a shared reference to mutable static
 --> static_mut.rs:7:34
  |
7 |         println!("tickets sold: {SOLD}");
  |                                  ^^^^ shared reference to mutable static
  |
  = note: shared references to mutable statics are dangerous; it's undefined behavior if the static is mutated or if a mutable reference is created for it while the shared reference lives
  = help: use a type that relies on "interior mutability" instead; to read more on this, visit <https://doc.rust-lang.org/reference/interior-mutability.html>
  = note: for more information, see <https://doc.rust-lang.org/edition-guide/rust-2024/static-mut-references.html>
  = note: `#[deny(static_mut_refs)]` (part of `#[deny(rust_2024_compatibility)]`) on by default

error: aborting due to 1 previous error

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O static_mut.rs && ./static_mut

The lint static_mut_refs is a warning in older editions and an error in 2024, because the compiler cannot check Rust’s rules for references on a value that any code may change at any moment. The help points to the way out, a type with A type that lets its contents change even through a shared reference, because the type itself keeps the change safe: by copying values in and out, by checks while the program runs, or with atomic instructions. Cell, RefCell, Mutex and the atomic types are examples. such as an atomic counter, which the concurrency module covers. Until then, a value that changes belongs in a let mut binding inside a function. Here are the three side by side:

Property let const static
Where it can be declared in a block anywhere anywhere
Type written out optional required required
Value worked out when the line runs when the program is built when the program is built
In memory while its block runs copied to every use one place for the whole run
May change with mut never with interior mutability, or as a static mut in unsafe

Checkpoint

Check: const and static

  1. Question 1 of 2 What does rustc do with const LIMIT = 6;?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It refuses it with "missing type for const item" and suggests a type

    Constants and statics need their type written: const LIMIT: u32 = 6;. rustc refuses a constant without one and suggests the type the value would have had.

  2. Question 2 of 2 A program needs a table of 5,000 fares that never changes, that every part of the program reads, and that should exist once in memory. What do you declare?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A static

    The Reference advises a const unless you need a single address, a large amount of data or interior mutability. A large, shared, read-only table is the case for a static.

A binding lives until the end of its block

Every { … } is a block, and a block is a scope: The part of a program where a name can be used. A let binding's scope runs from its let statement to the end of the block that contains it.: a binding made inside it can be used up to its closing brace and no further. That is why this loop, meant to add up three marks, ends with a total of 0:

loop_total.rs: a total shadowed inside the loop Rust · loop_total.rs
// Meant to add up three marks.
fn main() {
    let total = 0;
    for mark in [72, 85, 64] {
        let total = total + mark;
        println!("inside the loop: {total}");
    }
    println!("after the loop: {total}");
}

Output

inside the loop: 72
inside the loop: 85
inside the loop: 64
after the loop: 0

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O loop_total.rs && ./loop_total

The body of a for loop is a block of its own, run once per mark. let total = total + mark; makes a new binding there, which ends with each pass, so the outer total is never changed. No warning appears, because every name is used. The fix is the one from the first section: let mut total = 0; before the loop and total += mark; inside it.

When a scope ends, each of its bindings is dropped: its value’s clean-up runs, and memory it owns, such as the text of a String, is freed. Numbers have nothing to clean up, so to make drops visible this program uses a type that prints a line when one of its values is dropped. You will write types like it in later modules; here it only shows the order:

drops.rs: when each value is dropped Rust · drops.rs
// A ticket that prints a line when it is dropped, so each drop can be seen.
struct Ticket(&'static str);

impl Drop for Ticket {
    fn drop(&mut self) {
        println!("  dropped {}", self.0);
    }
}

fn main() {
    let _first = Ticket("ticket 1");
    let _ = Ticket("ticket 2");
    println!("after the line with _");
    {
        let _inner = Ticket("ticket 3");
        println!("end of the inner block");
    }
    let _last = Ticket("ticket 4");
    println!("end of main");
}

Output

  dropped ticket 2
after the line with _
end of the inner block
  dropped ticket 3
end of main
  dropped ticket 4
  dropped ticket 1

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O drops.rs && ./drops

Step through main and match each drop to the line of output it printed:

Interactive · step by step When each ticket in drops.rs is dropped Step through main. The left column is main's own block, the right column the line with _ and the inner block.
When each ticket in drops.rs is dropped, step 6 of 6. Showing _first (ticket 1, dropped last), let _ = … (ticket 2, dropped at once), _inner (inner block) (ticket 3, dropped at the }), _last (ticket 4, dropped first). In focus: _last, _first._firstticket 1, dropped lastlet _ = …ticket 2, dropped at once_inner (inner block)ticket 3, dropped at the }_lastticket 4, dropped first

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. A binding for ticket 1. let _first = Ticket("ticket 1"); binds the first ticket. A name that starts with an underscore is still a binding, so this ticket lives until the end of main.
  2. No binding for ticket 2. let _ = Ticket("ticket 2"); makes a ticket and binds nothing: _ is the wildcard pattern, which takes no value. Nothing keeps the ticket, so it is dropped at the end of that statement, and dropped ticket 2 is the first line printed.
  3. Inside the inner block. The inner block opens with {, and _inner binds ticket 3 inside it. end of the inner block is printed while ticket 3 still exists.
  4. The inner block ends. At the closing } the inner block ends, and its bindings go with it: dropped ticket 3 is printed while main is still running.
  5. A binding for ticket 4. let _last = Ticket("ticket 4"); binds the last ticket in main's own block, after ticket 1, and end of main is printed.
  6. main ends. At the end of main its bindings are dropped in the reverse order they were made: dropped ticket 4, then dropped ticket 1. The last binding made is the first one dropped.

Three rules came out of that run. A block’s bindings are dropped when it ends, in the reverse order they were made: ticket 4 before ticket 1. An inner block drops its own bindings at its closing brace, while the outer block goes on. And let _ = … makes no binding at all: _ is the wildcard pattern: The pattern _, which matches any value without binding it. let _ = make(); keeps nothing, so the value made there is dropped at the end of that statement., which matches a value without taking it, so a value made on that line is dropped at the end of that statement. A name that starts with an underscore, such as _first, is an ordinary binding. Unused names get warnings, and this session shows them for a small program:

unused.rs Rust · unused.rs
// Three names: one never changed, one never used, one meant to be unused.
fn main() {
    let mut fare = 2_500;
    let change = 500;
    let _receipt = "printed later";
    println!("fare: {fare} paise");
}
Warnings for an unused mut and an unused variable Python · unused_session.py
from session import run

# unused.rs built for debugging: rustc prints its warnings, then the program runs.
run("""
rustc --edition 2024 unused.rs
./unused
""")

Output

$ rustc --edition 2024 unused.rs
warning: variable does not need to be mutable
 --> unused.rs:3:9
  |
3 |     let mut fare = 2_500;
  |         ----^^^^
  |         |
  |         help: remove this `mut`
  |
  = note: `#[warn(unused_mut)]` (part of `#[warn(unused)]`) on by default

warning: unused variable: `change`
 --> unused.rs:4:9
  |
4 |     let change = 500;
  |         ^^^^^^ help: if this is intentional, prefix it with an underscore: `_change`
  |
  = note: `#[warn(unused_variables)]` (part of `#[warn(unused)]`) on by default

warning: 2 warnings emitted
$ ./unused
fare: 2500 paise

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 unused_session.py

fare never changes, so its mut gets a warning; change is never used, and the help suggests _change. _receipt gets no warning, because the underscore tells rustc the name is unused on purpose. Pick by what should happen to the value: _name keeps it until the block ends, _ lets it go at once. That difference matters for values whose drop does something, such as a lock that is released when its guard is dropped.

Key idea

A binding lives until the end of the block that made it, and a block’s bindings are dropped in reverse order; a _name keeps its value until then, while a bare _ keeps nothing, so a value made on its line is dropped at once.

Checkpoint

Check: Scopes, drops and unused names

  1. Question 1 of 2 In loop_total.rs, the marks become [10, 20, 30]. What does the line "after the loop" print?

    Type a number.

    A hint

    Which total does the line after the loop read?

    Show the answer: Question 1 of 2

    Answer: 0

    let total = total + mark; makes a new binding inside the loop's body, which ends with each pass. The outer total is never changed, so the line after the loop prints 0, whatever the marks are.

  2. Question 2 of 2 Which line keeps the ticket alive until the end of main?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: let _t = Ticket("t");

    A name that starts with an underscore is still a binding: it silences the unused-variable warning and keeps the value until its block ends. A bare _ makes no binding, so the value is dropped at once.

Practise: fix a ticket receipt

This program should print a receipt for three tickets, and it has six binding mistakes of the kinds this lesson showed. Copy it into a file, build it with rustc --edition 2024 receipt_broken.rs, and fix one error at a time. rustc reports the mistakes in rounds, because some checks run only after others pass, so build again after each fix:

receipt_broken.rs: six binding mistakes Rust · receipt_broken.rs
// A receipt for three tickets, with six binding mistakes.
const TICKET_PAISE = 2_500;
static gate: &str = "Gate 3";

fn main() {
    let count = " 3\n";
    count = count.trim();
    let count = count.parse().expect("not a whole number");
    let total = TICKET_PAISE * count;

    let discount = 0;
    if count >= 3 {
        let discount = 500;
    }
    total = total - discount;

    println!("{gate}: {count} tickets");
    println!("to pay: {total} paise");
}

Output (exit status 1)

Printed as an error (standard error)

error: missing type for `const` item
 --> receipt_broken.rs:2:19
  |
2 | const TICKET_PAISE = 2_500;
  |                   ^ help: provide a type for the constant: `: i32`

error: aborting due to 1 previous error

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O receipt_broken.rs && ./receipt_broken

It is finished when it builds with no warnings and prints two lines, Gate 3: 3 tickets and to pay: 7000 paise. One mistake builds without an error and only shows in the total, and the warning about an unused variable points at it. When your receipt matches, compare it with one way to do it:

receipt.rs: the six mistakes fixed Rust · receipt.rs
// The receipt with its six mistakes fixed.
const TICKET_PAISE: u32 = 2_500;
static GATE: &str = "Gate 3";

fn main() {
    let count = " 3\n";
    let count = count.trim();
    let count: u32 = count.parse().expect("not a whole number");
    let mut total = TICKET_PAISE * count;

    let mut discount = 0;
    if count >= 3 {
        discount = 500;
    }
    total -= discount;

    println!("{GATE}: {count} tickets");
    println!("to pay: {total} paise");
}

Output

Gate 3: 3 tickets
to pay: 7000 paise

Recorded with rustc 1.99.0 (b940084d7) on macOS 26 arm64. To run it yourself: rustc --edition 2024 -O receipt.rs && ./receipt

The fixes, in the order rustc reported them: the constant got its type; parse got : u32; the trimmed text became a new binding with let instead of an assignment; total became mut; the inner let discount became an assignment to the outer, now mutable, discount; and gate became GATE. Last comes the self-check. It opens with whatever you have answered least well so far, and the feedback on each choice names the slip behind it.

Practice that adapts to you

Practise this lesson

14 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 14 let mut fare = 2_500; and fare is never changed. What does rustc do?

    Choose one answer.

    Show the answer: Question 1 of 14

    Answer: It builds the program and warns that the variable does not need to be mutable

    A mut that nothing uses is the opposite of E0384: legal but pointless. rustc warns with "variable does not need to be mutable" and the help "remove this mut".

  2. Question 2 of 14 This does not compile. What is the smallest change that makes it build?

    Read the code, then choose one answer.

    let count = 0;
    count += 1;
    println!("{count}");
    Show the answer: Question 2 of 14

    Answer: Write let mut count = 0;

    += assigns, and assignment needs a mutable binding: let mut count = 0;. Shadowing with let count = count + 1; would also build here, because nothing else changes count.

  3. Question 3 of 14 A loop must count how many of 40 marks are above 35. Which binding should the count be?

    Choose one answer.

    Show the answer: Question 3 of 14

    Answer: A let mut binding declared before the loop and increased inside it

    A value that changes while a loop runs and is needed afterwards is a mutable binding declared before the loop. Shadowing inside the loop body makes a binding per pass, and const and static are for values fixed for the whole program.

  4. Question 4 of 14 After let total = 10; and then let total = total + 5;, what happened to the first value, 10?

    Choose one answer.

    Show the answer: Question 4 of 14

    Answer: It still exists until its block ends, but the name total now refers to 15

    let total = total + 5; reads the first total, makes 15 and binds it to a new total. The first binding is out of reach from then on, but its value stays until the end of its block.

  5. Question 5 of 14 let x = 5;, let x = x * 2;, then a block { let x = x + 1; }, then let x = x - 3;. What is x at the end?

    Type a number.

    A hint

    The binding made inside the block ends with the block.

    Show the answer: Question 5 of 14

    Answer: 7

    5, then 10. Inside the block a new x of 11 exists only until the closing brace, so after the block x is 10 again, and the last let makes 10 − 3 = 7.

  6. Question 6 of 14 A program reads a port number from a configuration line such as " 8080". It needs the line, the trimmed text and a u16. How should the code name them?

    Choose one answer.

    Show the answer: Question 6 of 14

    Answer: Shadow one name: let port = line.trim(); then let port: u16 = port.parse()…

    Shadowing is made for conversions like this: each let takes the previous value and gives the name a new, possibly differently typed, value, and every step stays immutable.

  7. Question 7 of 14 let price = 25.0; and nothing else decides. Which type does price get?

    Choose one answer.

    Show the answer: Question 7 of 14

    Answer: f64

    Integer literals default to i32 and floating-point literals to f64 when nothing in the program decides otherwise.

  8. Question 8 of 14 let fare: u64 = 2_500; then let total = fare * 4;. What type do the 4 and total have?

    Choose one answer.

    Show the answer: Question 8 of 14

    Answer: Both are u64, decided by fare

    Inference uses every fact the program gives. fare is a u64, the literal 4 takes the same type, and so does total.

  9. Question 9 of 14 You print a parsed number at once, println!("{}", text.parse().unwrap());, with no let to annotate. How do you say which type parse produces?

    Choose one answer.

    Show the answer: Question 9 of 14

    Answer: With the turbofish: println!("{}", text.parse::<u32>().unwrap());

    Where there is no binding to annotate, name the type on the method: parse::<u32>(). The same syntax works on other generic functions.

  10. Question 10 of 14 When is const DAY_PASS_PAISE: u32 = TICKET_PAISE * 8; worked out?

    Choose one answer.

    Show the answer: Question 10 of 14

    Answer: While the program is compiled, before it ever runs

    The Reference says free constants are always evaluated at compile time. consts.rs printed 20000 for DAY_PASS_PAISE, worked out from TICKET_PAISE by the compiler.

  11. Question 11 of 14 Why does static_mut.rs fail to build in Rust 2024?

    Choose one answer.

    Show the answer: Question 11 of 14

    Answer: println! takes a shared reference to the static mut, and the lint static_mut_refs is an error by default in 2024

    Reading and writing a static mut needs unsafe, and in Rust 2024 even a shared reference to one, which println! makes, is denied by default. Types with interior mutability, such as atomics, are the way out.

  12. Question 12 of 14 Put the lines that drops.rs prints in the order it prints them.

    Give each item its position, from 1 (first).

    Show the answer: Question 12 of 14

    Answer:

    1. dropped ticket 2
    2. after the line with _
    3. end of the inner block
    4. dropped ticket 3
    5. end of main
    6. dropped ticket 4
    7. dropped ticket 1

    Ticket 2 is dropped at once, because _ binds nothing. Ticket 3 goes when the inner block ends. At the end of main, the bindings are dropped in reverse order: ticket 4, then ticket 1.

  13. Question 13 of 14 What is the difference between let _receipt = make(); and let _ = make();?

    Choose one answer.

    Show the answer: Question 13 of 14

    Answer: _receipt is a binding that keeps the value until its block ends; _ binds nothing, so the value is dropped at once

    _name is an ordinary binding the compiler does not expect you to use. _ is a pattern that matches without binding, so a value created there has no owner after the statement and is dropped.

  14. Question 14 of 14 A function calls lock(), which returns a guard that releases a lock when it is dropped, then updates shared data. Which line keeps the lock held during the update?

    Choose one answer.

    Show the answer: Question 14 of 14

    Answer: let _guard = lock();

    A value whose drop does something must be kept in a binding for as long as its effect is needed. _guard does that; _ and a bare call drop the guard at the end of the statement.

Interview questions

Warm-up (fresher): what is the difference between shadowing a variable and making it mutable?

Both let one name refer to new values, but they do it differently. A mut binding is one binding whose value is replaced in place by assignment, and every new value must have the binding’s type. Shadowing writes let again: it makes a new binding that hides the old one from that line on, may have a different type, and is itself immutable unless declared mut; the old value is untouched and lives until its block ends. So a counter in a loop needs mut, because shadowing inside the loop body makes a binding that ends with each pass, while converting a line of input into a number is a natural use of shadowing. A good answer adds that shadowing in an inner block ends at the block, which is a common source of totals that never change.

Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.

  1. Fresher to mid

    A service needs three program-wide values: a limit of 500 requests a minute, known when it is built; a read-only table of 5,000 delivery zones; and a count of requests served so far, updated by every thread. How do you declare each in Rust 2024, and why?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Mid to senior

    A helper Timer::start(name) returns a value that logs the time elapsed when it is dropped. A teammate puts let _ = Timer::start("load"); at the top of a slow function, and every log line says 0 ms. Why, how do you fix it, and why did rustc not stop it?

    Model answer, Pro. First step and follow-ups

Key takeaways

  • A let binding keeps its first value: assigning to it again is error E0384, and let mut allows new values of the same type.
  • Shadowing with let makes a new binding under the same name, possibly of a new type, and leaves the old value alone until its block ends.
  • rustc infers every type from the value and its uses, defaulting to i32 and f64, and asks for an annotation, as E0284 does for parse, when nothing decides.
  • const values are computed when the program is built and copied to each use; a static has one address for the whole run, and references to a static mut are an error in Rust 2024.
  • A binding lives until the end of its block and bindings are dropped in reverse order; _name keeps a value until then, while _ keeps nothing, so a value made on its line is dropped at once.

Terms in this lesson

binding
A name tied to a value by let or by a pattern. It is immutable unless it is declared with mut, and it lasts until the end of the block that made it.
constant
A value declared with const and its type. The compiler works it out while it builds the program and copies it into every place that uses it, so a constant never changes.
drop
What happens to a value when the binding that owns it goes out of scope: its clean-up runs and any memory it owns is freed. A block's bindings are dropped in the reverse order they were made.
scope
The part of a program where a name can be used. A let binding's scope runs from its let statement to the end of the block that contains it.
shadowing
Declaring a new binding with the name of an existing one. From that line on the name refers to the new binding, which may have another type, while the old value stays as it was until its block ends.
wildcard pattern
The pattern _, which matches any value without binding it. let _ = make(); keeps nothing, so the value made there is dropped at the end of that statement.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.