Variables, Types, and Immutability by Default

In most languages, variables vary — that's the default, and you opt into constancy. Rust flips it: variables are immutable unless you say otherwise. That one inverted default, plus a strong static type system with inference, quietly shapes how Rust code is written and prevents a whole class of bugs before you meet ownership.

Before the hard ideas, the everyday ones: variables, types, and Rust’s distinctive immutability by default. These are where you first feel Rust’s personality — a strong static type system that mostly stays out of your way via inference, and a default that makes you opt into mutability. This post covers the basics you’ll use in every program, and the design choices behind them that foreshadow Rust’s safety focus.

Immutability by default

The first surprise: in Rust, variables are immutable by default. When you bind a value with let, you can’t reassign it unless you explicitly opt into mutability with mut:

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

let mut y = 5;
y = 6;           // OK: y is mutable

This inverts the default of most languages, where variables are mutable and you opt into constancy (with const/final). Rust’s reasoning is that immutability is safer and should be the default — an immutable value can’t be changed unexpectedly, which eliminates a class of bugs (accidental mutation) and makes code easier to reason about (you know a let x binding won’t change). You can have mutability — you just have to ask for it with mut, which makes mutation visible and intentional rather than the silent default.

This connects to Rust’s whole philosophy: make the safe thing the default and the potentially-dangerous thing explicit. Immutability by default means that when you do see mut, it signals “this changes” — a useful signal — and that most of your values are safely constant. It’s a small thing that shapes how Rust reads: mutation is marked, so you can trust the unmarked. (It also interacts with ownership and concurrency later — immutable data is easier to share safely.)

A related concept is shadowing — you can declare a new variable with the same name, which shadows the old one (this isn’t mutation; it’s a new binding):

let x = 5;
let x = x + 1;      // a new x, shadowing the old; x is now 6
let x = x * 2;      // another new x; x is now 12

Shadowing lets you transform a value through a sequence of immutable bindings (even changing its type), which is idiomatic Rust — you get the convenience of “reusing a name” without giving up immutability. It’s distinct from mut: shadowing creates new bindings, mut allows reassigning one binding.

A strong static type system, with inference

Rust is statically and strongly typed — every value has a type known at compile time, and the compiler checks types rigorously (no implicit surprising conversions). But — crucially for ergonomics — Rust has strong type inference, so you usually don’t have to write types explicitly; the compiler figures them out:

let x = 5;              // inferred as i32 (the default integer type)
let name = "Rust";      // inferred as &str
let pi = 3.14;          // inferred as f64

let count: u32 = 10;    // explicit type annotation when you want or need it

So Rust gives you the safety of static typing (types checked at compile time, catching type errors before running) with much of the convenience of dynamic typing (you rarely write types, because inference handles it). You add explicit type annotations when the compiler needs help or when you want to be specific (e.g. choosing a particular integer size). This combination — strong static types, inferred — is a sweet spot: bugs caught at compile time, without the verbosity of annotating everything.

The core types

Rust’s basic types are what you’d expect from a systems language, with an emphasis on being explicit about size:

Two Rust characteristics show up here. First, explicit sizing — you choose your integer/float sizes, reflecting systems-programming control over memory. Second, no implicit conversions — Rust won’t silently convert between types (e.g. i32 to i64); you convert explicitly (with as or conversions), which prevents a class of subtle bugs from surprising coercions. Rust is deliberately explicit about types, consistent with its “make things visible” philosophy.

Functions and basic flow

Rounding out the basics, functions and control flow are conventional but with Rust touches:

fn add(a: i32, b: i32) -> i32 {
    a + b            // no semicolon: this is the return expression
}

fn main() {
    let n = add(2, 3);
    if n > 4 {
        println!("big");
    } else {
        println!("small");
    }
}

These basics — immutability by default, inferred static types, explicit sizing, expression-orientation — are the everyday texture of Rust, and each reflects the language’s themes of safety and explicitness. With them in hand, the next post reaches Rust’s defining idea, the one everything has been building toward: ownership.

Key takeaways

Further reading

Sources & References