Variables & Data Types
Learn immutable-by-default variable bindings, mutability, constants, shadowing, and Rust's scalar and compound data types.
Introduction
Rust makes an unusual choice right from the first line you write: variables are immutable by default. This single decision, combined with a strong static type system, eliminates a surprising number of bugs before you even get to ownership.
This lesson covers variable bindings and mutability, constants, shadowing, and the scalar and compound types that make up Rust's type system.
- Why Rust variables are immutable unless explicitly marked `mut`.
- How constants differ from immutable variables.
- What shadowing is and how it differs from mutation.
- Rust's scalar types: integers, floats, booleans, and characters.
- Rust's compound types: tuples and arrays.
Variables Are Immutable by Default
When you declare a variable with `let`, it is immutable by default — once a value is bound, you cannot reassign it. To allow reassignment, you must explicitly add the `mut` keyword.
This is a deliberate design choice, not a limitation. Immutability by default means that when you read `let x = 5;` anywhere in a codebase, you know with certainty that `x` never changes — which makes code dramatically easier to reason about, especially as programs grow.
Constants
Constants, declared with `const`, are always immutable — you cannot even mark a constant `mut`. Unlike `let` bindings, constants always require an explicit type annotation, and by convention their names are written in `SCREAMING_SNAKE_CASE`. Constants can be declared in any scope, including the global scope, and their value must be computable at compile time.
Shadowing
Rust lets you declare a new variable with the same name as a previous one using `let` again — this is called shadowing. It creates an entirely new binding rather than mutating the old one, which means the new variable can even have a different type than the original.
This is different from `mut`: mutation changes a value in place and must keep the same type, while shadowing creates a fresh binding and can transform the value into something new entirely — a common pattern is converting a value while keeping the same conceptual name.
Scalar Types
Rust has four primary scalar types: integers, floating-point numbers, booleans, and characters. Integers come in signed (`i8` to `i128`) and unsigned (`u8` to `u128`) variants at several fixed sizes, plus pointer-sized `isize`/`usize` used mainly for indexing. Without an explicit type or context, Rust defaults integer literals to `i32`.
| Type | Size | Typical Use |
|---|---|---|
| i8 / u8 | 8-bit | Very small counters, raw byte values. |
| i32 / u32 | 32-bit (i32 is the default integer type) | General-purpose whole numbers. |
| i64 / u64 | 64-bit | Large counters, IDs, timestamps. |
| isize / usize | Pointer-sized (32 or 64-bit) | Indexing into arrays, slices, and Vecs. |
| f32 / f64 | 32-bit / 64-bit (f64 is the default) | Decimal numbers; f64 is the default float type. |
| bool | 1 byte | true or false. |
| char | 4 bytes | A single Unicode scalar value, not just ASCII. |
The `char` type is worth calling out specifically: unlike C, where a `char` is one byte, Rust's `char` is always 4 bytes and represents a full Unicode scalar value — so it can hold things like emoji or accented letters, not just ASCII characters.
Compound Types
Rust's two primitive compound types are tuples and arrays. A tuple groups together a fixed number of values of potentially different types, written as `(i32, f64, char)`, and its elements are accessed with `.0`, `.1`, and so on. An array is a fixed-size collection of values of the same type, written as `[i32; 5]`, allocated on the stack.
Code Example
fn main() { let x = 5; let mut y = 10; y += 1;
const MAX_POINTS: u32 = 100_000;
let spaces = " "; let spaces = spaces.len();
println!("x = {}, y = {}, MAX_POINTS = {}, spaces = {}", x, y, MAX_POINTS, spaces);}Click Run to see what this code prints.
Notice `spaces` is declared twice with `let` — the second `spaces` shadows the first, changing its type from `&str` to `usize` (the result of `.len()`). This compiles cleanly because shadowing creates a brand-new binding rather than mutating the original.
Common Mistakes
- Trying to reassign a plain `let` binding without `mut` — the compiler will reject it with a clear "cannot assign twice to immutable variable" error.
- Confusing shadowing with mutation — shadowing creates a new binding and can change type; `mut` changes a value in place and cannot change its type.
- Relying on integer overflow behavior — overflow panics in debug builds but silently wraps in release builds, so code that depends on wrapping is dangerously inconsistent unless you use explicit `wrapping_add`, `checked_add`, or `saturating_add`.
- Assuming a Rust `String`'s `.len()` counts characters — it actually counts UTF-8 bytes, which can differ from character count for non-ASCII text.
Best Practices
- Default to plain `let` (immutable) bindings, and only add `mut` when a value genuinely needs to change.
- Use underscores in large numeric literals (`100_000`) for readability — the compiler ignores them.
- Reach for shadowing instead of `mut` when transforming a value into a new type or meaning, keeping the old name conceptually.
- Add explicit type annotations when the inferred default (like `i32` or `f64`) isn't the type you actually want, e.g. `let x: u8 = 5;`.
Frequently Asked Questions
It's a reasonable general-purpose size on modern hardware — fast on both 32-bit and 64-bit systems, and large enough for most everyday values.
In debug builds, overflow causes a panic so bugs are caught early; in release builds, it wraps around using two's complement arithmetic by default, unless you use explicit checked/wrapping/saturating methods.
No — const values are always immutable, cannot be marked mut, and must always have an explicit type annotation.
Statically typed with strong type inference — you rarely write explicit types, but the compiler still checks everything before your program ever runs.
Key Takeaways
- Variables declared with `let` are immutable by default; add `mut` to allow reassignment.
- Constants (`const`) are always immutable, always require a type annotation, and use SCREAMING_SNAKE_CASE.
- Shadowing creates a new binding with `let` and can change type; mutation changes a value in place and cannot.
- Rust's scalar types are integers, floats, booleans, and 4-byte Unicode `char`; compound types include tuples and fixed-size arrays.
Summary
You now understand how Rust variables, constants, and shadowing work, and you've seen the core scalar and compound types you'll use constantly. Next comes the most important lesson in this course: ownership and borrowing — the system that makes Rust's memory safety possible.