02. Variables and Types

📋 Jump to Takeaways

🎁 What if every variable you created was immutable by default — you literally couldn't change it after assignment? Why would a language force that constraint on you?

Rust makes immutability the default because mutable state is the root of most bugs in concurrent programs. You opt in to mutation explicitly, making your intent clear to both the compiler and anyone reading your code.

let — Immutable by Default

When you declare a variable with let, it cannot be reassigned:

fn main() {
    let x = 5;
    println!("x is: {}", x);
    // x = 10;  // ERROR: cannot assign twice to immutable variable
}

This isn't a limitation — it's a guarantee. When you see let x = 5, you know x is 5 for its entire lifetime. No hunting through code to find where it changed.

let mut — Opting Into Mutation

When you need a variable to change, add mut:

fn main() {
    let mut counter = 0;
    println!("counter: {}", counter); // counter: 0

    counter += 1;
    println!("counter: {}", counter); // counter: 1

    counter += 1;
    println!("counter: {}", counter); // counter: 2
}

The mut keyword is a signal. It tells readers: "this value will change." Every mutation point in your program is now visible at the declaration site.

Shadowing

You can redeclare a variable with the same name using a new let. This is called shadowing — the new binding hides the previous one:

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

    let x = x + 10;
    println!("x: {}", x); // x: 15

    let x = x * 2;
    println!("x: {}", x); // x: 30
}

Each let x creates a brand new variable. The old one still exists in memory but is no longer accessible by that name.

Shadowing differs from mut because you can change the type:

fn main() {
    let spaces = "   ";         // &str
    let spaces = spaces.len();  // usize — different type, same name
    println!("spaces: {}", spaces); // spaces: 3
}

With mut, changing the type would be a compile error. Shadowing lets you transform a value and reuse a meaningful name.

Type Inference

Rust's compiler infers types from context. You don't always need annotations:

fn main() {
    let x = 42;        // inferred as i32
    let y = 3.14;      // inferred as f64
    let active = true; // inferred as bool
    let name = "Rust"; // inferred as &str

    println!("{} {} {} {}", x, y, active, name);
    // Output: 42 3.14 true Rust
}

The compiler chooses i32 for integers and f64 for floats when there's no other context. Inference keeps code concise without sacrificing type safety.

Explicit Type Annotations

When inference isn't enough or you want a specific type, annotate with a colon:

fn main() {
    let small: i8 = 127;          // 8-bit signed
    let big: i64 = 9_000_000_000; // 64-bit signed
    let precise: f32 = 3.14;      // 32-bit float (not the default f64)
    let byte: u8 = 255;           // 8-bit unsigned

    println!("small: {}", small);     // small: 127
    println!("big: {}", big);         // big: 9000000000
    println!("precise: {}", precise); // precise: 3.14
    println!("byte: {}", byte);       // byte: 255
}

Annotations are required when the compiler can't determine the type — for example, when parsing a string into a number.

Scalar Types — Integers

Rust provides integers at every power-of-two size, signed and unsigned:

Signed Unsigned Bits Range (signed)
i8 u8 8 -128 to 127
i16 u16 16 -32,768 to 32,767
i32 u32 32 -2B to 2B
i64 u64 64 huge
i128 u128 128 enormous
isize usize arch pointer-sized
fn main() {
    let a: u8 = 255;              // max for u8
    let b: i16 = -1000;           // negative needs signed
    let c: usize = 100;           // used for indexing

    println!("a: {}, b: {}, c: {}", a, b, c);
    // Output: a: 255, b: -1000, c: 100
}

isize and usize match your CPU architecture (64-bit on modern machines). Use usize for array indices and collection lengths.

Scalar Types — Floats, Bool, Char

fn main() {
    // Floating point
    let f1: f64 = 2.718281828;  // double precision (default)
    let f2: f32 = 3.14;        // single precision

    // Boolean
    let is_rust: bool = true;
    let is_slow: bool = false;

    // Character — 4 bytes, supports Unicode
    let letter: char = 'A';
    let emoji: char = '🦀';
    let chinese: char = '中';

    println!("{} {} {} {} {} {}", f1, f2, is_rust, is_slow, letter, emoji);
    // Output: 2.718281828 3.14 true false A 🦀

    println!("char: {}", chinese); // char: 中
}

char in Rust is 4 bytes and represents a Unicode scalar value — not just ASCII. This means emoji and international characters are first-class citizens.

Compound Types — Tuples

Tuples group multiple values of different types into one compound value with a fixed length:

fn main() {
    let point: (i32, i32) = (10, 20);
    let mixed: (i32, f64, bool) = (1, 3.14, true);

    // Access by index
    println!("x: {}, y: {}", point.0, point.1);
    // Output: x: 10, y: 20

    // Destructuring
    let (a, b, c) = mixed;
    println!("a: {}, b: {}, c: {}", a, b, c);
    // Output: a: 1, b: 3.14, c: true
}

Tuple indices start at 0 and use dot notation. Destructuring lets you unpack all elements into individual variables at once.

Compound Types — Arrays

Arrays hold multiple values of the same type with a fixed length:

fn main() {
    let numbers: [i32; 5] = [1, 2, 3, 4, 5];
    let zeros = [0; 3]; // [0, 0, 0] — repeat syntax

    println!("first: {}", numbers[0]);  // first: 1
    println!("last: {}", numbers[4]);   // last: 5
    println!("zeros: {:?}", zeros);     // zeros: [0, 0, 0]
    println!("length: {}", numbers.len()); // length: 5
}

Arrays are stack-allocated and fixed-size. If you access an index out of bounds, Rust panics at runtime rather than allowing undefined behavior. For dynamic-size collections, you'll use Vec<T> later.

const vs let

const values are compile-time constants. They differ from let in important ways:

const MAX_SCORE: u32 = 100;
const PI: f64 = 3.14159265358979;

fn main() {
    let current_score: u32 = 85;

    println!("Score: {} / {}", current_score, MAX_SCORE);
    // Output: Score: 85 / 100

    println!("Pi: {}", PI);
    // Output: Pi: 3.14159265358979
}

Key differences:

  • const requires a type annotation — always
  • const must be a compile-time computable value — no function calls
  • const can be declared in any scope, including global
  • const is inlined everywhere it's used — no memory address

Use const for values that are truly fixed for the entire program. Use let for everything else.

String Types — &str vs String

Rust has two primary string types. This is a brief introduction — strings get their own deep-dive later.

fn main() {
    // &str — string slice, immutable, usually hardcoded
    let greeting: &str = "hello";

    // String — heap-allocated, growable, owned
    let mut name = String::from("Rust");
    name.push_str(" language");

    println!("{}, {}!", greeting, name);
    // Output: hello, Rust language!

    println!("length: {}", name.len()); // length: 13
}

&str is a borrowed reference to string data — cheap to pass around. String owns its data on the heap and can grow. You'll understand the ownership distinction fully when you reach the ownership lesson.

Key Takeaways

  • let creates immutable bindings — reassignment is a compile error
  • let mut explicitly opts into mutation
  • Shadowing (let x = ... again) creates a new variable and can change the type
  • Rust infers types but you can annotate with : Type
  • Integer types: i8i128 (signed), u8u128 (unsigned)
  • Float types: f32 (single), f64 (double, default)
  • bool is true/false; char is 4-byte Unicode
  • Tuples hold mixed types with fixed length; arrays hold same type with fixed length
  • const is compile-time, requires type annotation, and lives globally
  • &str is a borrowed string slice; String is an owned, growable string

🎁 Next up: you'll write your own functions and discover that Rust doesn't need a return keyword — the last expression in a function is the return value. This expression-based design changes how you think about code flow entirely.

📝 Ready to test your knowledge?

Answer the quiz below to mark this lesson complete.

Spot something off? Report an issue

© 2026 ByteLearn.dev. Free courses for developers. · Privacy