Data Types
Reviewed & published by Brayan K
Rust is a systems programming language focused on speed, memory safety, and fearless concurrency — and because it is statically typed, every value has a known type at compile time.
Part of the free Rust course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll meet Rust's scalar types (integers, floats, booleans, chars) and its compound types (tuples and arrays), and learn how the compiler infers and checks them.
What You'll Learn in This Lesson
- Integer types and what i32 vs u8 mean
- Floating-point numbers with f64
- The bool and char scalar types
- How tuples group different types together
- How arrays store fixed-length same-typed lists
- Using {} and {:?} placeholders to print values
1️⃣ Scalar Types: Numbers, Booleans, and Chars
Scalar types represent a single value. Rust has four families: integers (whole numbers like i32 and u8), floating-point numbers (decimals like f64), booleans (true / false), and characters (a single Unicode value in single quotes). You can let Rust infer the type or annotate it explicitly with a colon.
fn main() {
// INTEGERS: whole numbers. i32 is the default signed 32-bit integer.
// u8, u32, i64 etc. specify signedness and size. usize is for indexing.
let age: i32 = 30;
let big: u64 = 9_000_000_000;
// FLOATS: numbers with decimals. f64 is the default (double precision).
let pi: f64 = 3.14159;
// BOOLEANS: only true or false.
let is_rust_fun: bool = true;
// CHARACTERS: a single Unicode value in single quotes (4 bytes, not 1).
let letter: char = 'R';
let emoji: char = '🦀'; // chars can hold any Unicode scalar value
println!("age = {}, big = {}", age, big);
println!("pi = {}, fun? {}", pi, is_rust_fun);
println!("letter = {}, emoji = {}", letter, emoji);
}The letter i or u tells you signed or unsigned, and the number tells you the bit width. When you don't annotate, integers default to i32 and floats to f64.
2️⃣ Compound Types: Tuples and Arrays
Compound types bundle several values into one. A tuple holds a fixed number of values that can be different types, accessed by position (.0, .1). An array holds a fixed number of values that are all the same type, accessed by index ([0]). You can also destructure a tuple into separate names.
fn main() {
// TUPLES group a fixed number of values of possibly DIFFERENT types.
let person: (&str, i32, f64) = ("Ada", 36, 1.7);
// Access tuple elements by index with a dot.
println!("Name: {}", person.0); // -> Name: Ada
println!("Age: {}", person.1); // -> Age: 36
// You can also DESTRUCTURE a tuple into separate variables.
let (name, age, height) = person;
println!("{} is {} and {}m tall", name, age, height);
// ARRAYS hold a fixed number of values of the SAME type.
let scores: [i32; 3] = [90, 85, 100]; // 3 i32 values
println!("First score: {}", scores[0]); // index with [n], starts at 0
println!("Array length: {}", scores.len());
// {:?} is the "debug" placeholder — it prints whole structures.
println!("All scores: {:?}", scores);
}Both tuples and arrays have a fixed length known at compile time. For a growable list you'd reach for a Vec, which you'll meet later. Notice {:?} printed the whole array at once.
Your turn. Fill in the blanks marked ___, then run it.
// 🎯 YOUR TURN — finish the program, then run it.
fn main() {
// 1) Annotate this as an unsigned 8-bit integer (0 to 255).
let level: ___ = 7; // 👉 the type name like u8 / i32 / etc.
// 2) A float for the price.
let price: f64 = 9.99;
// 3) Make a tuple holding a name and a score.
let player = ("Sam", 42);
// 4) Access the score (the second element) using its index.
println!("{} scored {}", player.0, player.___); // 👉 the index of 42
// 5) Print the level and price.
println!("Level {} costs {}", level, price);
// ✅ Expected output:
// Sam scored 42
// Level 7 costs 9.99
}Mini-Challenge: Describe a Book
Write it yourself from the outline and run it with cargo run. You'll combine a tuple, destructuring, and an array.
// 🎯 MINI-CHALLENGE: describe a book.
//
// 1. Make a tuple "book" with a title (&str), a page count (i32), and a
// rating (f64). For example ("Rust 101", 320, 4.8).
// 2. Destructure it into title, pages, and rating.
// 3. Print: "<title> has <pages> pages and is rated <rating>".
// 4. Make an array of three i32 chapter lengths and print it with {:?}.
// 5. Print the number of chapters using .len().
//
// ✅ Example output:
// Rust 101 has 320 pages and is rated 4.8
// Chapter lengths: [40, 55, 60]
// Chapters: 3
fn main() {
// your code here
}Common Errors (and the fix)
- "mismatched types" — You mixed types, e.g. assigned a float to an i32. Match the value to the declared type, or change the annotation.
- "cannot add `f64` to `i32`" — Rust won't silently mix number types. Convert explicitly, e.g. x as f64.
- "index out of bounds" — You indexed past the end of an array (e.g. arr[5] on a 3-element array). Remember indexes start at 0 and stop at len - 1.
- Using double quotes for a char — 'R' is a char; "R" is a string. Use single quotes for a single char.
Pro Tips
- 💡 Default to i32 and f64: they're the most common and well-optimised choices unless you have a reason to pick otherwise.
- 💡 Use {:?} to debug: when you want to peek at an array, tuple, or other structure, the debug placeholder shows the whole thing.
- 💡 Use usize for indexing: array and vector indexes are usize, sized to your machine.
📋 Quick Reference — Data Types
| Type | Example |
|---|---|
| Signed integer | let n: i32 = -5; |
| Unsigned integer | let n: u8 = 200; |
| Float | let x: f64 = 1.5; |
| Boolean | let b: bool = true; |
| Character | let c: char = 'A'; |
| Tuple | let t = (1, "a", 2.0); |
| Array | let a = [1, 2, 3]; |
🎉 Lesson Complete!
- ✅ Scalar types: integers (i32, u8), floats (f64), bool, and char
- ✅ Tuples group different types; arrays hold same-typed items of fixed length
- ✅ Access tuples with .0 and arrays with [0]
- ✅ {} prints for users; {:?} prints whole structures for debugging
Practice quiz
What is Rust's default integer type when none is annotated?
- u8
- i64
- i32
- usize
Answer: i32. Integers default to i32, which is usually the fastest on modern hardware.
What does the u in u8 mean?
- Unsigned (no negatives)
- Universal
- Unicode
- Unsafe
Answer: Unsigned (no negatives). u means unsigned, so a u8 holds 0 to 255 with no negative values.
What is the largest value a u8 can hold?
- 256
- 127
- 1024
- 255
Answer: 255. A u8 is 8 bits, holding 0 through 255 inclusive.
What is the default floating-point type in Rust?
- f32
- f64
- float
- double
Answer: f64. Floats default to f64, which is double precision.
How many bytes does a Rust char occupy?
- 4
- 1
- 2
- 8
Answer: 4. A char is a Unicode scalar value stored in 4 bytes, not 1 like C.
Which quotes denote a single char literal?
- Double quotes, like "R"
- Backticks
- Single quotes, like 'R'
- No quotes
Answer: Single quotes, like 'R'. 'R' is a char; "R" is a string slice.
What can hold a single emoji like the crab in Rust?
- A u8
- A char
- A bool
- An i32
Answer: A char. A char holds any Unicode scalar value, including emoji.
How do you access the first element of an array a?
- a.0
- a.first
- a(0)
- a[0]
Answer: a[0]. Arrays are indexed with square brackets starting at 0; tuples use .0 instead.
What does the {:?} placeholder do in println!?
- Prints nothing
- Prints using Debug formatting
- Rounds floats
- Escapes quotes
Answer: Prints using Debug formatting. {:?} uses Debug formatting and can print whole structures like arrays.
Which type should you use for indexing collections?
- i32
- u8
- usize
- f64
Answer: usize. usize is sized to the machine's pointer width and is used for indexing.
Continue this course
- Previous: Cargo, Crates & the Module Workflow
- Next: Operators & Expressions — Arithmetic, overflow handling, comparison, and everything-is-an-expression
- Quick reference: Rust cheat sheet › Basics
Frequently asked questions
Why does Rust have so many integer types like i32, u8, and u64?
Each integer type specifies two things: whether it can be negative (i for signed, u for unsigned) and how many bits it uses (8, 16, 32, 64, 128). Smaller types use less memory and larger ones hold bigger values, so you can choose exactly what a value needs — important in systems programming. When you don't care, i32 is the sensible default and is usually the fastest on modern hardware. There is also usize/isize, sized to your machine's pointer width, which is what you use for indexing collections.
What is the difference between a tuple and an array?
A tuple groups a fixed number of values that can each be a different type, like ("Ada", 36, 1.7), and you access elements by position with .0, .1, .2. An array holds a fixed number of values that must all be the same type, like [90, 85, 100], and you access elements by index with [0], [1], [2]. Use a tuple when you want to bundle a few related but differently-typed values; use an array when you have a list of same-typed items with a known length.
Why is a Rust char four bytes instead of one?
In Rust a char represents a single Unicode scalar value, not a single byte. That means it can hold any character from any language, plus emoji like 🦀, so it is stored as four bytes. This is different from C, where char is one byte. If you are working with raw bytes you would use the u8 type instead, but for human-readable characters char gives you full Unicode support.
What does the {:?} placeholder do in println!?
The plain {} placeholder uses a type's Display formatting, meant for clean user-facing output. The {:?} placeholder uses Debug formatting, which is meant for developers and can print whole structures like arrays, tuples, vectors, and structs that derive Debug. So println!("{:?}", [1, 2, 3]) prints [1, 2, 3]. There is also {:#?} for a pretty-printed, multi-line version, which is great for inspecting larger data.