← Back to Rust series
πŸ¦€
Basics
Basics Β· Prerequisite: lesson 01

02. Variables, Constants, Shadowing, Type Inference

Rust variables are immutable by default. You add `mut` to make them mutable. This design improves multi-thread safety and code readability at the same time, and shadowing lets you reuse names without `mut`. This lesson also covers the difference between `let` and `const`, and when to annotate types vs. trust inference.

Rustvariablesletmutconstshadowingtype inference
Duration
⏱ ~1 hour
Level
πŸ“Š Beginner
Prerequisite
🎯 Lesson 01
OUTCOME
Rust variables are immutable by default. You add `mut` to make them mutable. This design improves multi-thread safety and code readability at the same time, and shadowing lets you reuse names without `mut`. This lesson also covers the difference between `let` and `const`, and when to annotate types vs. trust inference.

What you'll learn

  • 1Create variable bindings with `let` and confirm they're immutable by default
  • 2Use `mut` to make a variable mutable
  • 3Explain the difference between `const` and `let`
  • 4Use shadowing to reuse a name across types
  • 5Know when type annotations are required vs. inferred

Overview

The first surprise for people coming from other languages: `let x = 5; x = 6;` is a compile error in Rust. Immutability by default isn't a style choice β€” it's the **starting point for race-free concurrency**. If a value shared across threads is provably immutable, half your locks just disappeared.

Core Concepts

1) let β€” the basic binding

Form: `let name = value;`. Once bound, can't be reassigned.

rust
let x = 5;
x = 6; // error[E0384]: cannot assign twice to immutable variable `x`

2) mut β€” opt in to mutability

Form: `let mut name = value;`. The mutability has to be declared **at the binding site**.

rust
let mut x = 5;
x = 6;             // OK
println!("{}", x); // 6

3) const β€” compile-time constants

  • Type annotation is **required**: `const PI: f64 = 3.14;`
  • Allowed anywhere (module top level, function scope, ...)
  • Value must be evaluable at compile time (no runtime function calls)
  • Convention is **SCREAMING_SNAKE_CASE**

4) shadowing β€” rebind a name

Declaring `let` with the same name creates a fresh binding that hides the previous one. Unlike `mut`, **the new binding can change types**.

rust
let spaces = "   ";
let spaces = spaces.len(); // &str -> usize, perfectly fine
println!("{}", spaces);    // 3
Propertymutshadowing
Reuses same storageβœ“βœ— (fresh binding)
Type change allowedβœ—βœ“
Reverts at end of scopeβœ—βœ“

Hands-on Examples

Type inference mixed with explicit annotation β€” integer literals default to i32 but can be coerced:

rust
fn main() {
    let a = 10;            // inferred: i32
    let b: i64 = 10;       // explicit: i64
    let c = 10_u8;         // suffix: u8
    let pi: f64 = 3.14;
    println!("a={}, b={}, c={}, pi={}", a, b, c, pi);
}

Shadowing cleans up parse flows:

rust
fn main() {
    let input = "42";           // &str
    let input: i32 = input        // shadow -> i32
        .trim()
        .parse()
        .expect("not a number");
    println!("{}", input + 1);   // 43
}

Common Mistakes

Q. Does `mut` let me change the variable's type too?

A. No. `mut` only allows changing the **value**, not the type. To change types use shadowing.

Q. What's the difference between `const` and `let`?

A. `const` is evaluated at compile time and always requires a type annotation. `let` evaluates any runtime expression. Also `const` is never mutable β€” there's no such thing as `mut const`.

Q. Isn't immutability inconvenient?

A. Surprisingly, ~80% of everyday values are write-once. Adding `mut` only where you actually mutate makes intent explicit and unlocks better multi-thread safety.

Recap

  • `let` = immutable by default, add `mut` to opt in
  • `const` = compile-time constant, type annotation required
  • Shadowing = fresh binding, can even change type
  • Type inference is strong, but explicit annotations help readability

Try It Yourself

  1. Attempt to reassign an immutable variable to read the error, then fix with `mut`
  2. Use shadowing to parse a trimmed string like `" 3.14 "` into f64
  3. Define a `const` for Ο€ and reference it inside a function
Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub β†—