Variables in Rust

Essential
Last updated: Tags: Rust

Rust calls variable declarations bindings for a reason: let x = 5 binds the name x to the value 5. The binding is, by default, immutable — you can read x, but you cannot change it.

The let keyword

Every variable declaration starts with let:

let x = 5;
let greeting = String::from("hello");

The type is usually inferred. When it cannot be inferred, or when you want to be explicit, add a type annotation after the name:

let x: i32 = 5;
let bytes: Vec<u8> = Vec::new();

Immutability by default

Rust variables are immutable by default. This code is a compile error:

let x = 5;
x = 6; // error: cannot assign twice to immutable variable

The compiler enforces immutability so that a reader can trust that a value will not change. When you see let x = 5 without mut, you know x will always be 5 in that scope — no need to scan the rest of the function to confirm.

To allow reassignment, add mut:

let mut x = 5;
x = 6; // ok

Use mut when the binding needs to change. Leave it off when it does not.

Shadowing

Rust allows shadowing: a second let with the same name creates a new binding that hides the first:

let x = 5;
let x = x + 1;  // new binding, shadows old x
let x = x * 2;  // another new binding
println!("{}", x); // 12

Shadowing differs from mutation. Each let x creates a fresh binding — the old one is simply unreachable. Importantly, the new binding can have a different type:

let guess = "42";                              // &str
let guess: u32 = guess.parse().expect("NaN"); // u32, same name

With mut, the type could not change — a mutation must preserve the type. Shadowing is idiomatic when transforming a value through a pipeline and the intermediate bindings are not needed afterward.

Scope

A binding lives from its declaration to the end of the block containing it. When execution leaves the block, the value is dropped — its memory is freed or any owned resource is released:

{
    let s = String::from("hello");
    println!("{}", s);
} // s is dropped here; the heap allocation is freed

This is Rust’s RAII in action. The scope determines the lifetime of the binding, and the compiler ensures values are cleaned up at the right moment.

Type inference

Rust infers types from context:

let x = 5;           // i32 (integer literal default)
let y = 5.0;         // f64 (float literal default)
let mut v = Vec::new();
v.push(1u8);         // v is now Vec<u8>, inferred from the push

Integer literals without context default to i32; float literals default to f64. When inference has no basis, the compiler asks for an annotation:

let v: Vec<u8> = Vec::new(); // no push to infer from, annotation required

Add a type suffix — 5u8, 3.14f32 — to override the default on a literal.