Rust Basics #2 Variables and Types: let and mut, Shadowing, Functions That End in Expressions

5 min read

In Part 1 we saw that reassigning a variable declared with let is a compile error. This part starts from exactly that point. Variables, types, and functions are the basics, but Rust made different choices on all three — and those choices become the foundation for understanding ownership later.

let and mut: why immutable is the default #

src/main.rs
let count = 1;      // immutable — no reassignment
let mut total = 0;  // mutable — reassignment allowed
total += count;

In most languages, “variables change by default; only constants get special marking.” Rust is the reverse: not changing is the default, and only variables that change carry mut. When you read code, a variable without mut is guaranteed to keep its declared value to the end, so you only need to track the places where values can change — the amount of code you must read shrinks. The borrowing rules in Part 4 are also built on this immutable/mutable distinction, so mut is not a convenience marker but an axis that runs through the whole language.

const exists separately. It is a true constant fixed at compile time, requires a type annotation, and is written in uppercase by convention.

src/main.rs
const MAX_RETRIES: u32 = 3;

Shadowing: redeclaring the same name #

There is one way to appear to change a value without mut: redeclaring the same name with let, called shadowing.

src/main.rs
let input = "42";               // type &str
let input: i32 = input.parse().unwrap(); // same name, now type i32

Unlike reassignment, this creates a new variable that hides the previous one, so even the type can change. The pattern above — receiving input as a string and parsing it into a number — is the classic case. Rather than multiplying names like input_str and input_num, keeping the same name is the Rust idiom. It also guarantees, at the level of names, that the pre-conversion value will not be used afterwards.

Scalar types: integers come in sizes #

CategoryTypesNotes
Integersi8i128, u8u128, isize, usizedefault inference is i32
Floating pointf32, f64default inference is f64
Booleanbooltrue, false
Characterchara 4-byte Unicode scalar

The integer list looks long, but the practical instinct is simple: i32 unless there is a reason otherwise, usize (a pointer-sized unsigned integer) for collection indexes and lengths, and u8 for byte data. The fact that char is not one byte but a 4-byte Unicode scalar will come back in Part 7, where we handle strings.

Overflow behavior is worth knowing. In debug builds, integer overflow panics immediately; in release builds it wraps around and keeps going. The design intent is to catch “silently becoming a weird value” during development. When wrapping or saturating is what you actually want, you say so with explicit methods like wrapping_add and saturating_add.

Type inference is strong, so annotations are often omitted, but where inference cannot decide (like the parse result above), an annotation is required.

Tuples and arrays: fixed-size bundles #

src/main.rs
let point: (f64, f64) = (3.0, 4.0);
let x = point.0;                 // access by position
let (px, py) = point;            // destructuring

let days: [u32; 3] = [31, 28, 31]; // the length is part of the type
let jan = days[0];

A tuple is a fixed bundle of possibly different types; an array is a fixed-length bundle of one type. In both, the length is part of the type, so the size is settled at compile time. The variable-length collection you will use far more in practice, Vec, comes in Part 7 after ownership. If an array index goes out of bounds, Rust does not read the neighboring memory like C — it stops immediately with a panic. This is the same consistent attitude: loud failure over quiet misbehavior.

Functions: the last expression is the return value #

src/main.rs
fn add(a: i32, b: i32) -> i32 {
    a + b
}

Parameter types and the return type (->) are always written out, never inferred — the signature is the documentation. The eye-catching part is that there is no return, and it is not a typo. In Rust, if the last expression of a block has no semicolon, its value becomes the value of the block. Add a semicolon after a + b and it becomes a statement that discards the value, producing a compile error along the lines of “expected i32, found ()” — with a help line telling you to remove the semicolon, so there is no need to panic.

if is an expression too, so it produces a value. That is why there is no separate ternary operator.

src/main.rs
let price = if is_member { 800 } else { 1000 };

return is used only for early exits in the middle of a function. This “almost everything is an expression” property shows its full value with match in Part 5.

Summary #

  • Variables are immutable by default; only the ones that change carry mut. It shrinks what a reader has to track, and it underlies the borrowing rules later.
  • Shadowing redeclares the same name with let and can even change the type. It keeps parsing and conversion code free of name clutter.
  • The practical defaults: i32 for integers, usize for indexes, u8 for bytes. Overflow panics in debug and wraps in release.
  • The last expression of a block is its value. A semicolon separates statements from expressions, and if produces values too.
  • The next part is the steep hill and the heart of this course: ownership — how memory gets cleaned up without a GC.
X