Data Types
Rust's type system from the ground up — integer sizes and overflow, floats, booleans, characters, tuples, and arrays, with the rules that govern each.
Rust Is Statically Typed
Every value in Rust has a type known at compile time. The compiler uses this information to guarantee memory safety and catch bugs before your program runs. You've seen type inference at work — now let's understand what types actually exist.
Rust's types split into two categories:
- Scalar types — a single value: integers, floats, booleans, characters
- Compound types — multiple values grouped: tuples, arrays
Integer Types
Integers come in signed (i) and unsigned (u) variants, across six sizes:
| Length | Signed | Unsigned |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| arch | isize | usize |
- Signed integers can be negative. An
i8holds -128 to 127. - Unsigned integers are always non-negative. A
u8holds 0 to 255. isize/usizematch the pointer size of the target architecture (64-bit on modern machines).usizeis used for indexing and collection lengths.
let a: i32 = -1_000_000; // underscores improve readability
let b: u8 = 255;
let c: usize = arr.len(); // always the right type for indexingDefault: When you write let x = 5; with no annotation, Rust infers i32. It's the fastest integer type on most platforms and a safe default.
Integer Literals
Rust supports multiple literal formats:
let decimal = 1_000_000; // underscores anywhere for readability
let hex = 0xff; // 255
let octal = 0o77; // 63
let binary = 0b1010_0001; // 161
let byte = b'A'; // u8 only: 65Integer Overflow
In debug mode (the default with cargo build), integer overflow panics at runtime:
let x: u8 = 255;
let y = x + 1; // thread 'main' panicked: attempt to add with overflowIn release mode (cargo build --release), overflow wraps silently (two's complement). This matches C behavior but can produce silently wrong results.
If you need explicit wrapping, saturating, or checked arithmetic, use the methods Rust provides:
let x: u8 = 255;
x.wrapping_add(1) // 0 — wraps around
x.saturating_add(1) // 255 — clamps at max
x.checked_add(1) // None — returns Option<u8>
x.overflowing_add(1) // (0, true) — value + did it overflow?These make the intent explicit and work the same in both debug and release.
Floating-Point Types
Rust has two floating-point types, both IEEE 754:
| Type | Size | Precision |
|---|---|---|
f32 | 32-bit | ~7 decimal digits |
f64 | 64-bit | ~15 decimal digits |
let x = 2.0; // f64 by default
let y: f32 = 3.14; // explicit f32Use f64 by default — it's the same speed as f32 on modern CPUs and significantly more precise. Use f32 only when memory layout matters (GPU data, packed structs).
Floating-Point Gotchas
let x = 0.1_f64 + 0.2_f64;
println!("{x}"); // 0.30000000000000004 — not 0.3This is not a Rust bug. It's how IEEE 754 works. Never compare floats with == for exact equality — use an epsilon comparison:
fn approx_eq(a: f64, b: f64) -> bool {
(a - b).abs() < f64::EPSILON
}Basic Arithmetic
let sum = 5 + 10; // i32: 15
let difference = 95.5 - 4.3; // f64: 91.2
let product = 4 * 30; // i32: 120
let quotient = 56.7 / 32.2; // f64: 1.760...
let remainder = 43 % 5; // i32: 3Integer division truncates toward zero: 7 / 2 == 3, not 3.5.
Boolean Type
let t = true;
let f: bool = false; // explicit annotation
if t {
println!("it's true");
}Booleans are 1 byte. Unlike C, Rust does not treat integers as booleans — if 1 {} is a compile error. Conditions must be explicitly bool.
Character Type
char in Rust is a Unicode scalar value — 4 bytes, not 1. It can represent any Unicode character, not just ASCII.
let c = 'z';
let z = 'ℤ'; // mathematical integer sign
let heart = '❤'; // emoji
let cat = '🐱';Use single quotes for char, double quotes for &str/String. They are different types and not interchangeable.
let letter: char = 'a';
let word: &str = "hello";Tuple Type
A tuple groups values of different types into one compound value. The length is fixed at compile time.
let tup: (i32, f64, bool) = (500, 6.4, true);Destructuring unpacks a tuple into named bindings:
let (x, y, z) = tup;
println!("y = {y}"); // 6.4Index access uses .0, .1, etc.:
let x = tup.0; // 500
let y = tup.1; // 6.4The unit type () is an empty tuple. It's what functions return when they have no explicit return value — similar to void in C, but it's an actual type with one actual value.
fn do_nothing() -> () {} // explicit unit return
fn also_nothing() {} // implicit unit return — same thingArray Type
An array holds multiple values of the same type. The length is fixed at compile time and stored on the stack.
let arr = [1, 2, 3, 4, 5]; // type: [i32; 5]
let zeros = [0; 10]; // ten zeros: [0, 0, 0, ..., 0]
let months: [&str; 12] = [
"January", "February", "March",
"April", "May", "June",
"July", "August", "September",
"October", "November", "December",
];Access by index:
let first = arr[0]; // 1
let second = arr[1]; // 2Out-of-bounds access panics at runtime (in debug mode) or is checked via bounds checking (always on, even in release):
let arr = [1, 2, 3];
let i = 10;
println!("{}", arr[i]); // thread 'main' panicked: index out of boundsRust never silently reads out-of-bounds memory — this is one of the safety guarantees enforced at runtime (the compile-time checks catch many cases, but not runtime indices).
Array vs Vec
[T; N] (array) has a fixed length known at compile time, stored on the stack.
Vec<T> has a variable length, stored on the heap.
Use arrays for fixed-size, stack-allocated data (pixel buffers, small lookup tables). Use Vec for anything whose size varies at runtime. You'll learn Vec in the Collections article.
Type Casting
Rust does not implicitly coerce between numeric types. You must use as for explicit casts:
let x: i32 = 1000;
let y = x as u8; // 232 — truncates, wraps around
let z = x as f64; // 1000.0
let f = 3.99_f64;
let i = f as i32; // 3 — truncates toward zero, never roundsas casts are infallible but lossy — they never panic but can silently truncate. For safe numeric conversions with error handling, use TryFrom/TryInto:
use std::convert::TryFrom;
let big: i32 = 1000;
let small = u8::try_from(big); // Err(TryFromIntError)
let fits: i32 = 100;
let ok = u8::try_from(fits); // Ok(100)Summary
| Type | Description | Default |
|---|---|---|
i8–i128, isize | Signed integers | i32 |
u8–u128, usize | Unsigned integers | — |
f32, f64 | Floating-point | f64 |
bool | Boolean | — |
char | Unicode scalar (4 bytes) | — |
(T, U, ...) | Tuple — fixed, mixed types | — |
[T; N] | Array — fixed length, same type | — |
Key rules:
- No implicit type coercion — use
asorTryFrom - Integer overflow panics in debug, wraps in release — use checked methods for control
charis 4 bytes (Unicode), not 1 byte (ASCII)- Arrays are fixed-size stack values;
Vecis the growable heap alternative
Next: Functions — how to define and call functions, what ownership means for parameters and return values, and how expressions vs statements shape Rust's syntax.