Rust · Beginner

Data Types in Rust

9 min readUpdated September 24, 2026Every example verified

In short: Every Rust value has a type fixed at compile time. Scalars are integers (i32, u64 and so on), floats (f32, f64), bool and char; tuples and arrays group values. Numeric types never convert implicitly, so mixing them needs an explicit as cast.

Types are fixed and explicit

Rust is statically typed: the compiler knows the type of every value before the program runs, and it never changes. Most of the time you do not write the types, because the compiler infers them from literals and from how a value is used, but they are always there. When two types meet in an expression they must match exactly. Adding an i32 to an i64 is an error, not a silent widening, and dividing an integer by a float is an error too. The fix is an explicit conversion, usually with the as keyword, and the rule exists so that a program's arithmetic means what it says on every platform.

Integers come in signed and unsigned flavours of several widths: i8, i16, i32, i64, i128 and their unsigned partners u8 to u128. isize and usize match the machine's pointer width and are what Rust uses for indexes and lengths, which is why .len() returns a usize. An unconstrained integer literal defaults to i32. Literals may carry a suffix (255u8) and underscores for readability (4_000_000_000). Integer division truncates toward zero, so 17 / 5 is 3, and % gives the remainder. If an operation overflows its type, a debug build panics and a release build wraps around; when overflow is a real possibility, use methods such as checked_add or saturating_add to say what you want.

Floats are f32 and f64; a literal like 2.5 is an f64. bool is true or false and nothing else: Rust has no idea of a truthy number. char is a single Unicode scalar value written in single quotes, four bytes wide, so 'é' is one char even though it occupies two bytes inside a string. Text lives in two types, the borrowed string slice &str that a literal has, and the owned, growable String; the ownership lesson explains the relationship between them.

Tuples group a fixed number of values of possibly different types: ("Bed 7", 2.5, true). You reach the parts with .0, .1 and so on, or destructure the whole tuple into names with let (name, width, sunny) = plot;. The empty tuple () is the type of a function that returns nothing. Arrays group a fixed number of values of one type, [i32; 5], stored inline with the length as part of the type, so an array cannot grow. Indexing starts at 0, and an index past the end stops the program with a panic rather than reading memory it should not. For a list that grows, use Vec, covered in collections and iterators.

Input arrives as text, so reading a number means parsing. "42".parse::<u32>() converts a string to a u32 and returns a Result, because the text may not be a number; in these lessons .unwrap() accepts the value and stops the program on bad input. Trim the line first, since a stray space makes parsing fail.

Syntax

 Rust · syntax
let count = 3;                 // i32 by default
let big: u64 = 4_000_000_000;  // the annotation picks the type
let ratio = 2.5;               // f64 by default
let single: f32 = 2.5;
let sunny: bool = true;
let grade: char = 'B';         // single quotes for char
let name: &str = "Bed 7";      // double quotes for text

let plot: (&str, f64, bool) = ("Bed 7", 2.5, true);
let width = plot.1;            // 2.5
let temps: [i32; 3] = [18, 21, 19];
let first = temps[0];
let zeros = [0; 8];            // eight zeros

let whole = 7 / 2;             // 3: integer division truncates
let exact = 7 as f64 / 2.0;    // 3.5: convert first
let parsed: u32 = "42".trim().parse().unwrap();

A type annotation follows a colon after the name. as converts between numeric types, and parse() turns text into whichever number type the annotation asks for.

Integers, floats and the as cast

packets as f64 is what makes the multiplication on the fourth line legal.

 Rust
fn main() {
    let packets: u32 = 7;
    let grams_each: f64 = 12.5;
    let total_grams = packets as f64 * grams_each;
    println!("Seed weight: {} g", total_grams);
    println!("Seed weight: {:.2} g", total_grams);

    let seeds = 17;
    let rows = 5;
    println!("Seeds per row: {}", seeds / rows);
    println!("Left over: {}", seeds % rows);
    println!("Exact: {}", seeds as f64 / rows as f64);

    let big: u64 = 4_000_000_000;
    let small: u8 = 255;
    println!("{} and {}", big, small);
}

Output

Seed weight: 87.5 g
Seed weight: 87.50 g
Seeds per row: 3
Left over: 2
Exact: 3.4
4000000000 and 255

packets is a u32 and grams_each an f64; multiplying them directly would be an error, so packets as f64 converts the count first. {} prints the shortest text that represents the float exactly, while {:.2} fixes two decimal places. seeds / rows is integer division and drops the fraction; % keeps it as a remainder, and converting both operands with as f64 gives the exact quotient. The last two lines show a u64 holding a value too large for i32 and a u8 at its maximum of 255.

char, bool and tuples

Two characters, one comparison and a three-part tuple that is taken apart in two ways.

 Rust
fn main() {
    let grade = 'B';
    let accent = 'é';
    println!("{} takes {} byte(s), {} takes {}", grade, grade.len_utf8(), accent, accent.len_utf8());
    println!("Is a letter: {}", grade.is_alphabetic());

    let stock = 3;
    let low = stock < 5;
    println!("Low stock: {}", low);

    let entry = ("Harbour Lights", 1998, 4.5);
    let (title, year, rating) = entry;
    println!("{} ({}) rated {}", title, year, rating);
    println!("First field: {}", entry.0);
}

Output

B takes 1 byte(s), é takes 2
Is a letter: true
Low stock: true
Harbour Lights (1998) rated 4.5
First field: Harbour Lights

A char is one Unicode character, and len_utf8() reports how many bytes it needs when stored in a string: one for B, two for é. stock < 5 produces a bool, which prints as true. The tuple entry mixes a string slice, an integer and a float; let (title, year, rating) = entry; destructures it into three variables in one step, and entry.0 reads a single field by position.

Arrays and numbers read from input

The two input lines are 4 and 1.5; each is parsed into the type its annotation names.

 Rust
use std::io::BufRead;

fn main() {
    let mut lines = std::io::stdin().lock().lines();
    let beds: u32 = lines.next().unwrap().unwrap().trim().parse().unwrap();
    let side: f64 = lines.next().unwrap().unwrap().trim().parse().unwrap();
    println!("{} beds of {} m", beds, side);

    let temps: [i32; 5] = [18, 21, 19, 24, 22];
    let mut sum = 0;
    for t in temps {
        sum += t;
    }
    println!("Readings: {}", temps.len());
    println!("Average: {:.1}", sum as f64 / temps.len() as f64);
    let zeros = [0; 3];
    println!("{:?} {:?}", temps, zeros);
}

Input given to the program: 41.5

Output

4 beds of 1.5 m
Readings: 5
Average: 20.8
[18, 21, 19, 24, 22] [0, 0, 0]

The annotations on beds and side tell parse() which type to produce, and trim() guards against stray spaces. temps is an array of exactly five i32 values; for t in temps visits each one, temps.len() returns a usize, and both operands are cast to f64 before the division so the average keeps its fraction. [0; 3] builds an array of three zeros. Arrays print with {:?}, the Debug format, since they have no plain {} representation.

Common mistakes

  • Mixing integer types in one expression

    Why it goes wrong: Rust has no implicit numeric promotion. a + b with an i32 and an i64 is error E0277, cannot add i64 to i32, because the compiler will not guess which width you meant.

    Fix: Convert explicitly with as or, for a widening that cannot lose data, i64::from(a).

     Rust · fix
    let a: i32 = 3;
    let b: i64 = 4;
    let sum = i64::from(a) + b;
  • Writing a char in double quotes

    Why it goes wrong: "B" is a &str, not a char, so let c: char = "B"; is a type mismatch (E0308). The compiler's help text points at the quotes.

    Fix: Use single quotes for a single character: 'B'.

  • Expecting integer division to produce a fraction

    Why it goes wrong: 7 / 2 is 3 because both operands are integers and the result is truncated toward zero. Nothing warns you; the program simply prints a whole number.

    Fix: Convert to a float before dividing when you need the fraction: 7 as f64 / 2.0.

  • Casting a float to an integer and expecting rounding

    Why it goes wrong: as truncates toward zero: 3.99 as i32 is 3 and -5.9 as i32 is -5. Values outside the target range saturate at its limits, so 300.7 as u8 is 255.

    Fix: Call .round(), .floor() or .ceil() on the float first, then cast.

     Rust · fix
    let boxes = (3.99_f64).round() as i32; // 4

Rust's primitive types

TypeHoldsLiteral example
i8 to i128, isizesigned integers of 8 to 128 bits, or pointer width-40, 12i64
u8 to u128, usizeunsigned integers; usize is used for indexes and lengths255u8, 4_000
f32, f64floating-point numbers; literals default to f642.5, 1e3
booltrue or falsestock < 5
charone Unicode scalar value, 4 bytes'B', 'é'
(A, B, C)a fixed group of values of any types("Bed 7", 2.5, true)
[T; N]exactly N values of one type, fixed length[18, 21, 19], [0; 8]
&str, Stringtext: a borrowed slice, or an owned growable string"Bed 7", String::from("Bed 7")

Where you use this

A greenhouse controller reads sensor readings as text, and the types decide what the program can do with them. Temperatures parse into f64 because they carry a fraction; the number of beds parses into u32 because a count cannot be negative, and if someone types -3 the parse fails instead of producing nonsense. A fixed set of five sensors fits an array, since the hardware will not grow overnight, and the average is computed after converting the sum to f64. Choosing u32 over i32 and f64 over u32 at the declaration is how you tell the compiler which mistakes to catch for you.

 Rust · in practice
let beds: u32 = line.trim().parse().unwrap();
let readings: [f64; 5] = [18.5, 21.0, 19.5, 24.0, 22.5];
let mean = readings.iter().sum::<f64>() / readings.len() as f64;

Key points

  • Every value has one type fixed at compile time; the compiler infers it when the value makes it obvious.
  • Integer types differ in width and sign; an unconstrained literal is i32, a float literal is f64, lengths and indexes are usize.
  • Numeric types never convert implicitly; use as, or From, to convert.
  • Integer division truncates toward zero and % gives the remainder.
  • bool is only true or false; char is one Unicode character in single quotes.
  • Tuples hold a fixed set of mixed types; arrays hold a fixed number of one type.
  • line.trim().parse::<T>() converts input text to a number and returns a Result.

Try it yourself

The program reads a number of tiles and a tile side length in metres and prints them as text. Parse tiles as a u32 and side as an f64, then add a line that prints Area: followed by tiles times side squared, with one decimal place.

Your program
use std::io::BufRead;

fn main() {
    let mut lines = std::io::stdin().lock().lines();
    let tiles = lines.next().unwrap().unwrap();
    let side = lines.next().unwrap().unwrap();
    println!("{} tiles of side {}", tiles.trim(), side.trim());
}
Input the program receives: 6 ↵ 0.5
Expected output: 6 tiles of side 0.5 Area: 1.5

Practise this

Open the Rust playground

Frequently asked questions

Which integer type should I use in Rust by default?

i32 is the default for a reason: it is fast on every platform and large enough for most counts. Use u32 or u64 when a value can never be negative and you want the compiler to reject negatives at parse time, i64 or u64 for sums that may exceed about two billion, and usize whenever the value indexes an array or a Vec, because that is the type indexing requires.

How do I convert a String or &str to a number in Rust?

Call parse() and tell it the target type, either through an annotation on the variable, let n: i32 = text.trim().parse().unwrap();, or with the turbofish form text.trim().parse::<i32>(). parse() returns a Result because the text may not be a valid number; unwrap() takes the value or stops the program, and the error handling lesson shows how to react gracefully instead.

What is the difference between an array and a Vec in Rust?

An array [T; N] has a length that is part of its type and fixed at compile time; it is stored inline, often on the stack, and cannot grow. A Vec<T> stores its elements on the heap and can push and pop at run time. Use an array for a small, fixed collection such as the days of the week, and a Vec for anything whose size depends on input.

Progress is stored only in this browser.

How this page was checked. Every program on it was run with rustc 1.94 at build time by the publishing checks, and the output shown is what it printed. Running Rust inside the browser is not available yet, so the Run button is absent rather than pretending; copy the code and run it with rustc 1.94 locally.