Data Types in Rust
Rust is a statically typed programming language — like C, C++, and Java. Every value in Rust has a specific data type that determines its size, the range of values it can hold, and its performance characteristics. This tells the compiler exactly how to work with that data.
There are two broad categories of data types in Rust: scalar and compound.
Data Type Categories
Type Inference
The Rust compiler can usually infer the type of a variable based on its value and how it is used. For example, if you write:
let xyz = 10;Rust automatically assigns the type i32, because that is the default integer type.
The table below shows the default inferred types for common literals in Rust:
| Literal / Value | Example | Inferred Type | Notes |
|---|---|---|---|
| Integer | 10, -5, 0 | i32 | Default integer type |
| Floating-point | 3.14, 2.0 | f64 | Default float type |
| String literal | "hello" | &str | String slice, not String |
| Owned string | String::from("hi") | String | Must be explicitly created |
| Boolean | true, false | bool | Only two possible values |
| Character | 'a', 'Z' | char | Unicode scalar value |
| Array | [1, 2, 3] | [i32; 3] | Uses default i32 |
| Tuple | (1, 2.0) | (i32, f64) | Each element inferred separately |
| Option | Some(5) | Option<i32> | Depends on the inner value |
| Result | Ok(10) | Result<i32, _> | Error type often unknown (_) |
When You Must Annotate the Type
Sometimes inference is not enough and you must tell Rust the type explicitly. A common example is the parse method, which can convert a string into many different types — so Rust needs you to specify which one:
fn main() {
let number_value: u32 = "12".parse().expect("Not a number!");
println!("{}", number_value);
}Without the : u32 annotation, the compiler produces this error:
error[E0282]: type annotations needed
--> src/main.rs:2:9
|
2 | let number_value = "12".parse().expect("Not a number!");
| ^^^^^^^^^^^^ ----- type must be known at this pointThe parse method converts a string value into a number. The expect method handles the Result it returns — if parsing succeeds it returns the value 12, otherwise the program panics with the message "Not a number!".
Scalar Types
A scalar type represents a single value. Rust has four scalar types:
- Integer
- Floating-point
- Boolean
- Character
Integer Types
An integer is a whole number with no decimal (fractional) part. In Rust, integer types are prefixed with either i (signed) or u (unsigned):
- Signed (
i) — can hold negative and positive values - Unsigned (
u) — can hold only zero and positive values
| Length | Signed | Unsigned |
|---|---|---|
| 8-bit | i8 | u8 |
| 16-bit | i16 | u16 |
| 32-bit | i32 | u32 |
| 64-bit | i64 | u64 |
| 128-bit | i128 | u128 |
| Architecture-dependent | isize | usize |
The value range of each type is determined by the number of bits it uses:
| Length | Signed Range | Unsigned Range |
|---|---|---|
| 8-bit | i8: −128 → 127 | u8: 0 → 255 |
| 16-bit | i16: −32,768 → 32,767 | u16: 0 → 65,535 |
| 32-bit | i32: −2,147,483,648 → 2,147,483,647 | u32: 0 → 4,294,967,295 |
| 64-bit | i64: −9,223,372,036,854,775,808 → 9,223,372,036,854,775,807 | u64: 0 → 18,446,744,073,709,551,615 |
| 128-bit | i128: −170,141,183,460,469,231,731,687,303,715,884,105,728 → 170,141,183,460,469,231,731,687,303,715,884,105,727 | u128: 0 → 340,282,366,920,938,463,463,374,607,431,768,211,455 |
| Arch-dependent | Same as i32 on 32-bit, i64 on 64-bit | Same as u32 on 32-bit, u64 on 64-bit |
Each signed variant can store numbers from −(2ⁿ⁻¹) to 2ⁿ⁻¹ − 1, where n is the number of bits. So i8 stores −128 to 127. Each unsigned variant stores 0 to 2ⁿ − 1, so u8 stores 0 to 255.
The isize and usize types depend on the architecture of the machine your program runs on — 64-bit on a 64-bit system and 32-bit on a 32-bit system. They are primarily used for indexing collections.
Integer Literals
You can write integer values in several formats. Rust also allows underscores (_) as visual separators to improve readability — for example 1_000_000 instead of 1000000:
| Format | Examples |
|---|---|
| Decimal | 1_000, 42, 7_654_321 |
| Hex | 0x1A, 0xFF, 0xdead_beef |
| Octal | 0o12, 0o755, 0o123_456 |
| Binary | 0b1010, 0b1101_0011, 0b0001_1110 |
Byte (u8 only) | b'A', b'z', b'0', b'\n' |
You can also append a type suffix directly to the literal to specify its type — for example, 46u8 means the number 46 stored as an unsigned 8-bit integer.
Integer Overflow
Integer overflow is an important concept to understand in Rust. If you try to assign a value beyond a type’s range — such as adding 1 to a u8 of value 255 — the behaviour depends on how the program is compiled:
- Debug mode — Rust detects the overflow and panics at runtime, helping you catch mistakes early.
- Release mode — Rust performs wrapping arithmetic, where the value wraps around to the start of the range. So
255 + 1becomes0.
fn main() {
let x: u8 = 255;
let y = x + 1; // overflow
println!("{}", y);
}
// Debug mode → program panics
// Release mode → output is 0To handle overflow safely and explicitly, Rust provides dedicated methods:
fn main() {
let x: u8 = 255;
let a = x.wrapping_add(1); // 0 — wraps around
let b = x.checked_add(1); // None — returns None on overflow
let c = x.overflowing_add(1); // (0, true) — value + overflow flag
let d = x.saturating_add(1); // 255 — clamps to the maximum
println!("{:?} {:?} {:?} {:?}", a, b, c, d);
}Use these methods whenever you need fine-grained control over overflow behaviour. They make your intent explicit and prevent unexpected results.
Floating-Point Types
Rust has two floating-point types: f32 and f64, representing 32-bit and 64-bit precision respectively.
f64is the default because modern CPUs handle 64-bit and 32-bit floats at roughly the same speed, andf64offers significantly more precision.- Rust has signed floats only — there is no unsigned floating-point type.
- Both types follow the IEEE-754 standard.
fn main() {
let a = 21.8; // f64 — inferred default
let b: f32 = 21.9; // f32 — explicitly annotated
}Boolean Type
Like most programming languages, Rust’s Boolean type has exactly two possible values: true and false. A Boolean is one byte in size and is represented by the keyword bool.
fn main() {
let is_active = true;
let is_done: bool = false; // with explicit type annotation
}Booleans are most commonly used in conditional expressions such as if / else and while loops.
Character Type
The char type is Rust’s most primitive way to represent a single Unicode character. Unlike strings, a char always holds exactly one character — whether that is a letter, digit, symbol, or emoji.
// char uses single quotes; &str / String uses double quotes
let c: char = 'A';
let s: &str = "Hello";Size and Unicode Support
Rust’s char is 4 bytes (32 bits) in size — large enough to hold any Unicode scalar value. This means a char can represent far more than just ASCII:
- ASCII letters:
'A','z' - Accented letters:
'é','ñ' - CJK characters:
'你','日' - Emojis:
'💖','🌍' - Zero-width spaces and special symbols
Valid Unicode scalar values in Rust range from U+0000 to U+D7FF and U+E000 to U+10FFFF. The range U+D800 to U+DFFF is excluded — those are surrogate pairs, which are only valid within UTF-16 and cannot stand alone as characters.
A char is not the same as a single byte. Because char is always 4 bytes, a string of 5 characters takes 5 char values — but may take fewer bytes when encoded as UTF-8.
Compound Types
Compound types can group multiple values into a single type. Rust has two built-in primitive compound types: tuples and arrays.
Tuple Type
A tuple is a fixed-size collection of values that can have different types. Once declared, a tuple cannot grow or shrink.
fn main() {
let tuple_value: (i32, f64, u8) = (109, 5.8, 12);
}Tuples are written as a comma-separated list of values inside parentheses. Each position in the tuple has its own type — which is why a tuple can mix i32, f64, and u8 in a single value.
Destructuring a Tuple
The most common way to read values out of a tuple is destructuring:
fn main() {
let tuple_value = (109, 5.8, 12);
let (x, y, z) = tuple_value;
println!("The value of y is: {y}");
}The let (x, y, z) = tuple_value line breaks the tuple apart into three separate variables. You can now use x, y, and z individually anywhere in the code.
Accessing Tuple Elements by Index
You can also access individual elements using dot notation followed by the zero-based index:
fn main() {
let x: (i32, f64, u8) = (109, 5.8, 12);
let first = x.0; // 109
let second = x.1; // 5.8
let third = x.2; // 12
}The Unit Type
A tuple with no values — written () — is called the unit type. It acts as a placeholder or as the implicit return type of functions that return nothing.
fn main() {
let empty = ();
println!("This is the unit value: {:?}", empty);
}Array Type
An array is another way to store a collection of multiple values. Unlike a tuple, every element in an array must have the same type, and arrays in Rust always have a fixed length.
fn main() {
let arr = [11, 22, 33, 44, 55];
}Stack vs Heap
Arrays are allocated on the stack — making them very fast. This contrasts with vectors, which are stored on the heap and can grow or shrink dynamically.
| Array | Vector | |
|---|---|---|
| Size | Fixed at compile time | Grows/shrinks at runtime |
| Storage | Stack | Heap |
| Speed | Very fast | Slightly slower |
| Use when | Size is known and constant | Size may change |
If you are unsure whether to use an array or a vector, choose a vector. Arrays are best when the number of elements is fixed and known at compile time — for example, the days of the week.
fn main() {
// A vector — can grow dynamically
let mut vec = vec![1, 2, 3];
vec.push(4);
println!("{}", vec[3]); // 4
}Type Annotation for Arrays
You can annotate an array’s type using square brackets containing the element type and the length, separated by a semicolon:
let a: [f64; 5] = [12.4, 2.4, 3.6, 34.9, 1.45];
// ^^^ ^
// type lengthRepeat Initialisation
To create an array where every element has the same value, use a shorthand with an initial value, a semicolon, and the desired length:
fn main() {
let a = [1; 4]; // [1, 1, 1, 1]
println!("{:?}", a);
}Accessing and Updating Array Elements
Array elements are accessed using zero-based index notation. If the array is declared as mut, its elements can also be updated:
fn main() {
let mut arr = [14, 12, 33, 44];
let first = arr[0];
let second = arr[1];
println!("Before update: first = {}, second = {}", first, second);
arr[0] = 100;
arr[1] = 200;
println!("After update: first = {}, second = {}", arr[0], arr[1]);
}Runtime Index Checking and Memory Safety
Rust checks array indices at runtime. If you access an index that is out of bounds, the program panics immediately rather than reading from arbitrary memory — a key part of Rust’s memory safety guarantee.
Here is an example that reads an index from the command line:
use std::io;
fn main() {
let a = [13, 42, 33, 34, 25];
println!("Please enter an array index.");
let mut index = String::new();
io::stdin()
.read_line(&mut index)
.expect("Failed to read line");
let index: usize = index
.trim()
.parse()
.expect("Index entered was not a number");
let element = a[index];
println!("The value of the element at index {index} is: {element}");
}If you enter an index that does not exist (e.g., 7 for a 5-element array), Rust panics with a clear error message:
thread 'main' panicked at src/main.rs:19:19:
index out of bounds: the len is 5 but the index is 7
note: run with `RUST_BACKTRACE=1` environment variable to display a backtraceIn languages like C and C++, accessing an out-of-bounds index can silently corrupt memory or crash the program in unpredictable ways. Rust prevents this by stopping the program immediately, before any unsafe memory access can occur.
Summary
| Type | Category | Key Characteristics |
|---|---|---|
i8–i128, isize | Scalar / Integer | Signed whole numbers |
u8–u128, usize | Scalar / Integer | Unsigned whole numbers |
f32, f64 | Scalar / Float | Decimal numbers (IEEE-754) |
bool | Scalar | true or false, 1 byte |
char | Scalar | Single Unicode character, 4 bytes |
(T1, T2, ...) | Compound / Tuple | Fixed-size, mixed types |
[T; N] | Compound / Array | Fixed-size, single type, stack-allocated |
What’s Next?
Now that you understand Rust’s type system, you are ready to explore Functions — where you will learn how to define, call, and pass data between functions in Rust.