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.
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.
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**.
let mut x = 5;
x = 6; // OK
println!("{}", x); // 63) 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**.
let spaces = " ";
let spaces = spaces.len(); // &str -> usize, perfectly fine
println!("{}", spaces); // 3| Property | mut | shadowing |
|---|---|---|
| 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:
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:
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
- Attempt to reassign an immutable variable to read the error, then fix with `mut`
- Use shadowing to parse a trimmed string like `" 3.14 "` into f64
- Define a `const` for Ο and reference it inside a function
All lecture materials and example code are openly available on GitHub.
View on GitHub β