TechLead
Lesson 2 of 28
5 min read
Rust

Variables, Types & Mutability

Master Rust variables with let and let mut, type inference, scalar types, compound types, constants, and shadowing fundamentals.

Variables and Mutability

In Rust, variables are immutable by default. This is a deliberate design choice that encourages you to write safer code. To make a variable mutable, you must explicitly opt in with let mut.

fn main() {
    // Immutable by default
    let x = 5;
    // x = 6; // ERROR: cannot assign twice to immutable variable

    // Explicitly mutable
    let mut y = 10;
    println!("y = {y}");
    y = 20; // OK — y is mutable
    println!("y = {y}");

    // Constants: MUST have type annotation, evaluated at compile time
    const MAX_POINTS: u32 = 100_000;
    const PI: f64 = 3.141_592_653_589_793;
    println!("Max: {MAX_POINTS}, PI: {PI}");
}

Immutable by Default: Why?

When a variable is immutable, the compiler can reason about your code better, other readers know the value won't change, and entire classes of bugs are eliminated. Mutability is the exception, not the rule — you opt into complexity only when needed.

Shadowing

Rust allows you to shadow a variable by declaring a new let binding with the same name. This is different from mut — shadowing creates a brand-new variable and can even change the type.

fn main() {
    let x = 5;
    let x = x + 1;       // x is now 6 (shadowed)
    let x = x * 2;       // x is now 12 (shadowed again)
    println!("x = {x}"); // 12

    // Shadowing can change the type!
    let spaces = "   ";         // &str
    let spaces = spaces.len();  // now usize
    println!("spaces = {spaces}");

    // This would NOT work with mut:
    // let mut spaces = "   ";
    // spaces = spaces.len(); // ERROR: mismatched types
}

Scalar Types

Rust has four primary scalar types: integers, floating-point numbers, booleans, and characters.

Integer Types

Length Signed Unsigned Range (Signed)
8-biti8u8-128 to 127
16-biti16u16-32,768 to 32,767
32-biti32u32-2B to 2B
64-biti64u64Very large
128-biti128u128Extremely large
archisizeusizeDepends on platform (32/64-bit)
fn main() {
    // Integer literals
    let decimal = 98_222;       // Underscores for readability
    let hex = 0xff;             // Hexadecimal
    let octal = 0o77;           // Octal
    let binary = 0b1111_0000;   // Binary
    let byte = b'A';            // Byte (u8 only)

    // Explicit type annotations
    let x: i32 = 42;
    let y: u64 = 1_000_000;
    let z: i8 = -128;

    // Floating-point types
    let f1 = 2.0;      // f64 (default)
    let f2: f32 = 3.0; // f32

    // Boolean
    let t: bool = true;
    let f = false;

    // Character (4 bytes, Unicode scalar value)
    let c = 'z';
    let heart = '❤';
    let emoji = '🦀'; // The Rust mascot: Ferris the crab!

    println!("{decimal} {hex} {octal} {binary} {byte}");
    println!("{f1} {f2} {t} {f} {c} {heart} {emoji}");
}

Compound Types

fn main() {
    // Tuples: fixed-size, mixed types
    let tup: (i32, f64, bool) = (500, 6.4, true);
    let (x, y, z) = tup;                // Destructuring
    println!("x={x}, y={y}, z={z}");
    println!("First: {}", tup.0);       // Index access

    // Unit type: empty tuple ()
    let _unit: () = ();

    // Arrays: fixed-size, same type, stack-allocated
    let arr = [1, 2, 3, 4, 5];
    let first = arr[0];
    let second = arr[1];
    println!("first={first}, second={second}");

    // Array with repeated value
    let zeros = [0; 5]; // [0, 0, 0, 0, 0]

    // Type-annotated array
    let months: [&str; 4] = ["Jan", "Feb", "Mar", "Apr"];

    // Rust checks array bounds at runtime!
    // let crash = arr[10]; // panic: index out of bounds

    println!("{:?} {:?}", zeros, months);
}

Type Inference & Annotations

fn main() {
    // Rust infers types from context
    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

    // Sometimes you MUST annotate
    let guess: u32 = "42".parse().expect("Not a number!");

    // Numeric operations
    let sum = 5 + 10;
    let difference = 95.5 - 4.3;
    let product = 4 * 30;
    let quotient = 56.7 / 32.2;
    let remainder = 43 % 5;

    // Type casting with 'as'
    let x: i32 = 42;
    let y: f64 = x as f64;
    let z: u8 = 255;

    println!("{sum} {difference} {product} {quotient} {remainder}");
    println!("{y} {z}");
}

Key Takeaways

  • ✅ Variables are immutable by default — use let mut for mutability
  • ✅ Shadowing creates new variables and can change types
  • ✅ Rust has rich scalar types: integers (i8-i128, u8-u128), floats, bool, char
  • ✅ Compound types include tuples (mixed types) and arrays (same type, fixed size)
  • ✅ Type inference is powerful, but annotations are needed for ambiguous cases

Continue Learning