Free Handbook · Every example compiled & verified

Fundamentals

Variables and mut, shadowing, integer and float types, overflow in debug vs release, tuples, arrays, constants, println! formatting and reading input.

0 / 140 lessons🔥 0 day streak
ShareXLinkedIn

Module 01 · what you'll be able to do

  • Declare immutable and mutable variables and explain why Rust makes immutability the default
  • Pick the right integer or float type and predict what overflow does in debug and release builds
  • Group values in tuples and fixed-size arrays and destructure them
  • Format output with {}, {:?}, {:#?}, width, precision and alignment
  • Read a line from standard input and parse it into a number with parse::()
01

Variables and mutability

let binds a name to a value. By default that binding is immutable: once set, it cannot change. You opt in to change with let mut. This is the reverse of most languages, and it is deliberate — when you read Rust code, a plain let is a promise that the value stays put, and mut flags exactly the places where it does not.

rustmain.rs
fn main() {
    let name = "Ferris";        // immutable
    let mut score = 10;          // mutable
    println!("{name} starts with {score}");

    score += 5;
    score = score * 2;
    println!("{name} now has {score}");

    let level: u8 = 3;           // explicit type annotation
    println!("level {level}");
}
Outputcompiled & run with real Rust
Ferris starts with 10
Ferris now has 30
level 3
Your turn

Remove mut from score and compile. Read the error — it is the one on the card below.

Error you will hit

E0384: assigning twice to an immutable variable

rust
fn main() {
    let score = 10;
    score += 5;
    println!("{score}");
}
error[E0384]: cannot assign twice to immutable variable `score`
 --> main.rs:3:5
  |
2 |     let score = 10;
  |         ----- first assignment to `score`
3 |     score += 5;
  |     ^^^^^^^^^^ cannot assign twice to immutable variable
  |
help: consider making this binding mutable
  |
2 |     let mut score = 10;
  |         +++
Why the compiler said that

A binding without mut can be assigned exactly once. The compiler refuses the second assignment rather than letting a value you promised was fixed change behind your back.

The fix

If the value really needs to change, declare it let mut. If you only need a new value derived from the old one, shadow it (next lesson).

rust
fn main() {
    let mut score = 10;
    score += 5;
    println!("{score}");
}
Type inference
You rarely need to write a type on a let. Rust infers score as i32 (the default integer type) from the literal 10. You add an annotation like let level: u8 = 3; when you want a different type or the compiler cannot work it out, which happens most often with parse() and collect().
02

Shadowing

You can declare a new variable with the same name as an old one. The new one shadows the old from that point on. Unlike mut, shadowing creates a brand-new variable, so it may even have a different type. It is the idiomatic way to transform a value in steps — for example, text read from input turned into a number — without inventing names like input_str and input_num.

rustmain.rs
fn main() {
    let x = 5;
    let x = x + 1;          // new x shadows the old one
    {
        let x = x * 2;      // shadows only inside this block
        println!("inner x = {x}");
    }
    println!("outer x = {x}");

    let spaces = "   ";     // a &str
    let spaces = spaces.len(); // now a usize: a different type is fine
    println!("spaces = {spaces}");
}
Outputcompiled & run with real Rust
inner x = 12
outer x = 6
spaces = 3
VisualizeWhich x is whichStep 1 / 6
fn main() {
let x = 5;
let x = x + 1;
{
let x = x * 2;
println!("inner x = {x}");
}
println!("outer x = {x}");
}
Line 2

The first x is created.

Variables now
x5
All 6 steps as a table
StepLineWhat happenedVariables now
12The first x is created.x = 5
23A second x is created from the first. The first still exists but can no longer be named.x = 6
35Inside the block, a third x shadows the second.x = 12
46Print the innermost x.
57The block ends and the third x is dropped. The name refers to the second x again.x = 6
68Print the outer x — the block never changed it.
Error you will hit

E0308: mut cannot change a variable's type

rust
fn main() {
    let mut spaces = "   ";
    spaces = spaces.len();
    println!("{spaces}");
}
error[E0308]: mismatched types
 --> main.rs:3:14
  |
2 |     let mut spaces = "   ";
  |                      ----- expected due to this value
3 |     spaces = spaces.len();
  |              ^^^^^^^^^^^^ expected `&str`, found `usize`
Why the compiler said that

mut lets you change the value, never the type. spaces was inferred as &str, so assigning a usize to it is a type mismatch.

The fix

Use shadowing: let spaces = spaces.len(); declares a new variable that is allowed to have a new type.

rust
fn main() {
    let spaces = "   ";
    let spaces = spaces.len();
    println!("{spaces}");
}
03

Scalar types: integers, floats, bool, char

A scalar type holds a single value. Rust has four families, and the integer types carry their size and signedness in the name: i for signed, u for unsigned, then the number of bits.

TypeSizeRange / notes
i8, i16, i32, i64, i1288–128 bitsSigned. i32 is the default for integer literals.
u8, u16, u32, u64, u1288–128 bitsUnsigned, never negative. u8 is a byte: 0 to 255.
isize, usizepointer size64 bits on a 64-bit machine. usize is the type of every index and length.
f32, f6432 / 64 bitsIEEE-754 floats. f64 is the default.
bool1 bytetrue or false. Never converted from integers automatically.
char4 bytesOne Unicode scalar value: 'a', 'é', '中'. Single quotes.
rustmain.rs
fn main() {
    let big = 1_000_000;          // underscores for readability; i32
    let byte: u8 = 255;
    let hex = 0xff;               // 255
    let bin = 0b1010;             // 10
    let precise = 2.5_f64;        // suffix picks the type
    let half = 7 / 2;             // integer division truncates
    let exact = 7.0 / 2.0;
    let is_ok = true;
    let heart = '\u{2764}';

    println!("{big} {byte} {hex} {bin} {precise}");
    println!("7 / 2 = {half}, 7.0 / 2.0 = {exact}, 7 % 2 = {}", 7 % 2);
    println!("{is_ok} {heart}");
    println!("u8 goes from {} to {}", u8::MIN, u8::MAX);
    println!("i32::MAX = {}", i32::MAX);
}
Outputcompiled & run with real Rust
1000000 255 255 10 2.5
7 / 2 = 3, 7.0 / 2.0 = 3.5, 7 % 2 = 1
true ❤
u8 goes from 0 to 255
i32::MAX = 2147483647

Converting between number types with as

Rust never converts numbers implicitly — not even i32 to i64. You convert with as (which can truncate), or with From/TryFrom when you want the conversion checked.

rustmain.rs
fn main() {
    let a: i32 = 300;
    let b = a as i64;           // widening: always safe
    let c = a as u8;            // narrowing: keeps the low 8 bits (300 - 256)
    let d = 3.99_f64 as i32;    // float to int truncates toward zero
    let e = u8::try_from(a);    // checked conversion returns a Result
    let f = u8::try_from(200_i32);
    println!("{b} {c} {d}");
    println!("{:?} {:?}", e.is_err(), f);
}
Outputcompiled & run with real Rust
300 44 3
true Ok(200)
Your turn

Try let total: i64 = a + b; without any as. What does the compiler say about adding an i32 to an i64?

04

Integer overflow: debug vs release

What happens when a u8 holding 255 gets 1 added? In C it is silently wrong (or undefined behaviour for signed types). Rust picks a rule per build profile: in a debug build (cargo run, or plain rustc) the program panics, so you find the bug while testing; in a release build (--release) the checks are removed for speed and the value wraps around (255 + 1 = 0).

Error you will hit

Runtime panic: attempt to add with overflow

rust
fn main() {
    let input = "255";
    let level: u8 = input.parse().unwrap();
    let next = level + 1;
    println!("{next}");
}

thread 'main' (10303939) panicked at main.rs:4:16:
attempt to add with overflow
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Why the compiler said that

In a debug build every +, - and * on integers is checked. 255 + 1 does not fit in a u8, so the program stops with a panic that names the file, line and column. Compiled with --release (or -O), the same program prints 0 instead.

The fix

Decide what overflow should mean and say so explicitly with one of the methods below — or use a wider type such as u16 or u32.

rust
fn main() {
    let input = "255";
    let level: u8 = input.parse().unwrap();
    let next = level.saturating_add(1); // stays at 255
    println!("{next}");
}
rustmain.rs
fn main() {
    let x: u8 = 250;
    println!("wrapping_add:    {}", x.wrapping_add(10));
    println!("checked_add:     {:?}", x.checked_add(10));
    println!("checked_add ok:  {:?}", x.checked_add(5));
    println!("saturating_add:  {}", x.saturating_add(10));
    println!("overflowing_add: {:?}", x.overflowing_add(10));
}
Outputcompiled & run with real Rust
wrapping_add:    4
checked_add:     None
checked_add ok:  Some(255)
saturating_add:  255
overflowing_add: (4, true)

These methods behave the same in debug and release builds, so your intent is in the code, not in a build flag.

MethodReturnsUse when
wrapping_addThe wrapped valueHashes, checksums, ring-buffer indices
checked_addOption: None on overflowUser input or money, where overflow is an error to handle
saturating_addClamped to MIN/MAXHealth bars, volume, counters that should stop at the limit
overflowing_add(value, did_overflow)Big-number arithmetic that needs the carry
The compiler catches the obvious ones
Write let x: u8 = 255 + 1; with plain constants and you get a compile error (this arithmetic operation will overflow), not a panic. Runtime overflow happens with values the compiler cannot see ahead of time — input, parsing, loop counters.
05

Tuples and arrays

Rust has two built-in compound types. A tuple groups a fixed number of values of possibly different types: (i32, f64, char). An array holds a fixed number of values of the same type: [i32; 5] is five i32s. Both have a size known at compile time and live on the stack. For a list that grows, you want a Vec (Module 05).

rustmain.rs
fn main() {
    let point = (3, -2);
    let person: (&str, u32, f64) = ("Ada", 36, 1.65);

    let (x, y) = point;                     // destructuring
    println!("x = {x}, y = {y}");
    println!("{} is {} years old", person.0, person.1);

    let unit = ();                          // the empty tuple, "unit"
    println!("{:?}", unit);

    let primes = [2, 3, 5, 7, 11];
    let zeros = [0; 4];                     // [value; count]
    println!("first {}, last {}, len {}", primes[0], primes[4], primes.len());
    println!("{:?}", zeros);

    let mut grid = [[0u8; 3]; 2];           // 2 rows of 3
    grid[1][2] = 9;
    println!("{:?}", grid);
}
Outputcompiled & run with real Rust
x = 3, y = -2
Ada is 36 years old
()
first 2, last 11, len 5
[0, 0, 0, 0]
[[0, 0, 0], [0, 0, 9]]
Your turn

Loop over primes with for p in primes { ... } and print their sum.

Error you will hit

Array index out of bounds (caught at compile time)

rust
fn main() {
    let primes = [2, 3, 5, 7, 11];
    println!("{}", primes[5]);
}
error: this operation will panic at runtime
 --> main.rs:3:20
  |
3 |     println!("{}", primes[5]);
  |                    ^^^^^^^^^ index out of bounds: the length is 5 but the index is 5
  |
  = note: `#[deny(unconditional_panic)]` on by default
Why the compiler said that

Array indexes start at 0, so a five-element array has indexes 0 to 4. Here the index is a constant, so the compiler can prove the access is out of range and refuses to build. When the index is only known at runtime, Rust checks it then and panics with index out of bounds: the len is 5 but the index is 5 — it never reads memory past the end the way C can.

The fix

Use an index below len(), or primes.get(i), which returns None instead of panicking.

rust
fn main() {
    let primes = [2, 3, 5, 7, 11];
    println!("{:?} {:?}", primes.get(4), primes.get(5));
}
06

Constants and operators

A const is a value fixed at compile time. It must have a type annotation, its name is SCREAMING_SNAKE_CASE, and it can live outside any function. The compiler pastes the value in wherever it is used. A static is similar but has one fixed memory address for the whole program; you will rarely need one.

rustmain.rs
const MAX_PLAYERS: u32 = 4;
const SECONDS_PER_DAY: u64 = 60 * 60 * 24;
static GREETING: &str = "Welcome";

fn main() {
    println!("{GREETING}! Up to {MAX_PLAYERS} players.");
    println!("A week has {} seconds.", SECONDS_PER_DAY * 7);

    let a = 17;
    let b = 5;
    println!("{} {} {} {} {}", a + b, a - b, a * b, a / b, a % b);
    println!("{} {} {}", a > b, a == b, a != b);
    println!("{} {} {}", true && false, true || false, !true);
    println!("{} {} {}", a & b, a | b, a << 2);

    let mut count = 0;
    count += 1;           // there is no count++ in Rust
    count *= 10;
    println!("count = {count}");
    println!("2^10 = {}", 2_i32.pow(10));
}
Outputcompiled & run with real Rust
Welcome! Up to 4 players.
A week has 604800 seconds.
22 12 85 3 2
true false true
false true false
1 21 68
count = 10
2^10 = 1024
Error you will hit

Rust has no ++ operator

rust
fn main() {
    let mut count = 0;
    count++;
    println!("{count}");
}
error: Rust has no postfix increment operator
 --> main.rs:3:10
  |
3 |     count++;
  |          ^^ not a valid postfix operator
  |
help: use `+= 1` instead
  |
3 -     count++;
3 +     count += 1;
  |
Why the compiler said that

Rust left out ++ and -- on purpose: in C their pre/post versions are a classic source of confusing bugs (i = i++). One way to add one is enough.

The fix

Write count += 1;.

rust
fn main() {
    let mut count = 0;
    count += 1;
    println!("{count}");
}
No mixing types in arithmetic
Both sides of + must be the same type. 1_u8 + 1_u32 or 2 * 1.5 (an integer times a float) do not compile — convert one side with as first: 2.0 * 1.5 or n as f64 * 1.5.
07

Printing: {}, {:?} and {:#?}

The println! family uses one small formatting language. {} uses the Display format — the user-facing one, which only some types have. {:?} uses the Debug format — the programmer-facing one, available for almost every standard type, including tuples, arrays and Option. {:#?} is Debug, pretty-printed over several lines.

rustmain.rs
fn main() {
    let name = "Ada";
    let pi = 3.14159265;
    let scores = [90, 72, 85];
    let pair = ("x", 1);

    println!("Hi {name}, pi is roughly {pi:.2}");
    println!("{0} and {1}, then {0} again", "left", "right");
    println!("{:?}", scores);
    println!("{:?} {:?}", pair, Some(5));
    println!("{:#?}", pair);
    println!("[{:>6}] [{:<6}] [{:^6}]", "r", "l", "c");
    println!("[{:06.2}] [{:+}]", pi, 7);
    println!("{:x} {:X} {:b} {:o}", 255, 255, 5, 8);
    println!("{{ literal braces }}");
    let s = format!("{}-{}", "abc", 42);  // format! returns a String
    println!("{s}");
}
Outputcompiled & run with real Rust
Hi Ada, pi is roughly 3.14
left and right, then left again
[90, 72, 85]
("x", 1) Some(5)
(
    "x",
    1,
)
[     r] [l     ] [  c   ]
[003.14] [+7]
ff FF 101 10
{ literal braces }
abc-42
Your turn

Print scores with {} instead of {:?}. Arrays do not implement Display — read what the compiler tells you.

SpecMeaningExample → output
{}Display42 → 42
{:?} / {:#?}Debug / pretty Debug"hi" → "hi" (with quotes)
{:.2}Precision3.14159 → 3.14
{:>8} {:<8} {:^8}Right / left / centre in 8 columns
{:08.2}Zero-pad to width 8, 2 decimals3.14159 → 00003.14
{:x} {:b} {:o}Hex, binary, octal255 → ff
{name}Inline variable (Rust 2021)
Error you will hit

E0277: a type that does not implement Display

rust
fn main() {
    let scores = [90, 72, 85];
    println!("{}", scores);
}
error[E0277]: `[{integer}; 3]` doesn't implement `std::fmt::Display`
 --> main.rs:3:20
  |
3 |     println!("{}", scores);
  |               --   ^^^^^^ `[{integer}; 3]` cannot be formatted with the default formatter
  |               |
  |               required by this formatting parameter
  |
  = help: the trait `std::fmt::Display` is not implemented for `[{integer}; 3]`
  = note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
Why the compiler said that

{} needs the Display trait, and the standard library does not decide for you how an array should look to an end user. It does provide Debug.

The fix

Use {:?} for debugging output, or format the elements yourself. Your own types get Display by implementing it (Module 07) and Debug with #[derive(Debug)].

rust
fn main() {
    let scores = [90, 72, 85];
    println!("{:?}", scores);
}
print!, eprintln! and dbg!
print! prints without a newline. eprintln! writes to standard error, so it does not mix with output that is piped to a file. dbg!(expr) prints the file, line, expression and its value to stderr and returns the value — the quickest debugging tool there is.
08

Reading input and parsing numbers

Keyboard input comes from std::io::stdin(). read_line appends one line, including the trailing newline, to a String you pass in. You almost always call .trim() next, then .parse::<T>() to turn the text into a number. parse returns a Result, because the text might not be a number at all.

rustmain.rs
use std::io;

fn main() {
    let mut line = String::new();
    io::stdin().read_line(&mut line).expect("failed to read a line");
    let name = line.trim().to_string();

    line.clear();                          // read_line appends, so empty it first
    io::stdin().read_line(&mut line).expect("failed to read a line");
    let age: u32 = line.trim().parse().expect("age must be a whole number");

    println!("Hello, {name}!");
    println!("In ten years you will be {}.", age + 10);
}
Outputcompiled & run with real Rust
Hello, Ada!
In ten years you will be 46.

The page fed this program two lines of input: Ada and 36.

Your turn

Read a third line with a height in metres and parse it as an f64 with parse::<f64>().

rustmain.rs
use std::io::{self, BufRead};

fn main() {
    let stdin = io::stdin();
    let mut total = 0;
    for line in stdin.lock().lines() {
        let line = line.unwrap();
        match line.trim().parse::<i64>() {
            Ok(n) => total += n,
            Err(e) => println!("skipping {:?}: {e}", line),
        }
    }
    println!("total = {total}");
}
Outputcompiled & run with real Rust
skipping "abc": invalid digit found in string
total = 35

Reading every line until end of input, and handling bad lines instead of crashing. Input: 10, 20, abc, 5.

Error you will hit

E0284: parse() needs to know the target type

rust
fn main() {
    let n = "42".parse().unwrap();
    println!("{n}");
}
error[E0284]: type annotations needed
 --> main.rs:2:9
  |
2 |     let n = "42".parse().unwrap();
  |         ^        ----- type must be known at this point
  |
  = note: cannot satisfy `<_ as FromStr>::Err == _`
help: consider giving `n` an explicit type
  |
2 |     let n: /* Type */ = "42".parse().unwrap();
  |          ++++++++++++
Why the compiler said that

parse can produce many types — i32, u8, f64, bool, an IP address... Nothing in this code says which one, so the compiler cannot pick.

The fix

Annotate the variable (let n: i32 = ...) or use the turbofish syntax "42".parse::<i32>().

rust
fn main() {
    let n = "42".parse::<i32>().unwrap();
    println!("{n}");
}
Binding
A name attached to a value by let. Immutable unless declared mut.
Shadowing
Declaring a new variable with the same name as an existing one, hiding the old one. May change the type.
usize
The unsigned, pointer-sized integer used for every index and length.
Overflow
A result too big (or small) for its type. Panics in debug builds, wraps in release builds unless you use checked_/wrapping_/saturating_ methods.
Unit type
(), the empty tuple. The value of expressions that return nothing.
Display vs Debug
{} is the user-facing format, {:?} the programmer-facing one.
Turbofish
The ::<T> syntax that names a generic type at a call: parse::<i32>().
Quick check

A u8 variable holds 255. The program adds 1 using the plain + operator. What happens?

Quick check

What is the difference between let mut x and shadowing with a second let x?

Frequently asked questions

Why are Rust variables immutable by default?
Because most values never need to change, and code is easier to reason about (and to share safely between threads) when they cannot. Making mutation opt-in with mut means every place a value can change is visible when you read the code.
What is the difference between const and let in Rust?
const is evaluated at compile time, must have an explicit type, can be declared outside functions and is inlined wherever it is used. let creates a runtime variable inside a function, can be computed from anything, and can be made mutable with mut. const can never be mut.
How do I read an integer from input in Rust?
Read a line into a String with std::io::stdin().read_line(&mut line), then line.trim().parse::<i32>(). trim removes the newline, and parse returns a Result you handle with match, expect or ?.

Finish the Rust handbook, then get hired

Sit the exam for your certificate, run your resume through the ATS checker, and see the jobs that ask for exactly this.

Check my resume
Found this course useful? Share it.
ShareXLinkedIn

Comments

0

Join the conversation. Sign in to leave a comment — we'd love to hear your thoughts.