Guide contents

Lesson 6 of 94

Scalar Types: Integers, Floats, Booleans & Characters

A scalar type holds a single value: an integer, a floating-point number, a boolean, or a character. Rust's scalar types have three notable properties:

  1. Rust has unsigned integers (u8, u32, u64...).
  2. Rust never converts between number types automatically. Adding an i32 to an i64 is a compile error until you convert one of them.
  3. Integer overflow is not silent: it is a compile error when the compiler can see it, and a crash (panic) in debug builds when it happens at runtime.

The type table

Rust Size Range
i8 8 bits -128 to 127
i16 16 bits -32,768 to 32,767
i32 32 bits about ±2.1 billion
i64 64 bits about ±9.2 × 10¹⁸
i128 128 bits about ±1.7 × 10³⁸
isize pointer size 64 bits on 64-bit machines
u8 8 bits 0 to 255
u16 16 bits 0 to 65,535
u32 32 bits 0 to about 4.3 billion
u64 64 bits 0 to about 1.8 × 10¹⁹
u128 128 bits 0 to about 3.4 × 10³⁸
usize pointer size Used for lengths and indexes
f32 32 bits about 7 significant digits
f64 64 bits about 16 significant digits
bool 8 bits true, false
char 32 bits Any Unicode scalar value

The i means signed, u means unsigned, and the number is the size in bits.

1. Default integer and float types

With no annotation, an integer literal is i32 and a decimal literal is f64.

Rust
fn main() {
    let items = 12;
    let weight = 2.5;
    let big: i64 = 9_000_000_000;
    let tiny: i8 = -100;
    println!("{items} {weight} {big} {tiny}");
}
Output
12 2.5 9000000000 -100

2. The minimum and maximum of each type

Every integer type has MIN and MAX constants.

Rust
fn main() {
    println!("i8:   {} to {}", i8::MIN, i8::MAX);
    println!("u8:   {} to {}", u8::MIN, u8::MAX);
    println!("i32:  {} to {}", i32::MIN, i32::MAX);
    println!("u32:  {} to {}", u32::MIN, u32::MAX);
    println!("i64:  {} to {}", i64::MIN, i64::MAX);
    println!("u64:  {} to {}", u64::MIN, u64::MAX);
    println!("u128 max: {}", u128::MAX);
}
Output
i8:   -128 to 127
u8:   0 to 255
i32:  -2147483648 to 2147483647
u32:  0 to 4294967295
i64:  -9223372036854775808 to 9223372036854775807
u64:  0 to 18446744073709551615
u128 max: 340282366920938463463374607431768211455

3. Unsigned integers cannot be negative

Use an unsigned type when a negative value would be a bug: a quantity, an age, a byte, a length. The compiler rejects negative literals for unsigned types.

Rust✗ does not compile · E0600
fn main() {
    let quantity: u32 = -1;
    println!("{quantity}");
}
Output
error[E0600]: cannot apply unary operator `-` to type `u32`
 --> src/main.rs:2:25
  |
2 |     let quantity: u32 = -1;
  |                         ^^ cannot apply unary operator `-`
  |
  = note: unsigned values cannot be negated
help: you may have meant the maximum value of `u32`
  |
2 -     let quantity: u32 = -1;
2 +     let quantity: u32 = u32::MAX;
  |

Using u32 for a quantity makes a negative value impossible.

4. A literal that does not fit is an error

Rust✗ does not compile
fn main() {
    let level: u8 = 256;
    println!("{level}");
}
Output
error: literal out of range for `u8`
 --> src/main.rs:2:21
  |
2 |     let level: u8 = 256;
  |                     ^^^
  |
  = note: the literal `256` does not fit into the type `u8` whose range is `0..=255`
  = note: `#[deny(overflowing_literals)]` on by default

5. Integer literal forms

Rust supports decimal, hexadecimal, octal, and binary literals, byte literals, and type suffixes such as 10u8 or 5_i64.

Rust
fn main() {
    let million = 1_000_000;
    let hex = 0xFF;
    let octal = 0o755;
    let binary = 0b1010_1010;
    let byte = b'A';        // a u8 holding the ASCII code of 'A'
    let small = 200u8;      // suffix sets the type to u8
    let wide = 5_i64;       // suffix sets the type to i64
    println!("{million} {hex} {octal} {binary} {byte} {small} {wide}");
}
Output
1000000 255 493 170 65 200 5

6. Arithmetic operators

+, -, *, /, and % are the usual arithmetic operators. Integer division truncates toward zero, and % keeps the sign of the left operand.

Rust
fn main() {
    println!("7 / 2 = {}", 7 / 2);
    println!("-7 / 2 = {}", -7 / 2);
    println!("7 % 3 = {}", 7 % 3);
    println!("-7 % 3 = {}", -7 % 3);
    println!("7.0 / 2.0 = {}", 7.0 / 2.0);
}
Output
7 / 2 = 3
-7 / 2 = -3
7 % 3 = 1
-7 % 3 = -1
7.0 / 2.0 = 3.5

7. rem_euclid for a remainder that is never negative

For "wrap around" calculations such as days of the week or array positions, you usually want a remainder in 0..n. Rust has rem_euclid for this. It is a method, called with a dot.

Rust
fn main() {
    let today: i32 = 2; // 0 = Monday, so 2 = Wednesday
    let days_back: i32 = -10;
    let result = (today + days_back) % 7;
    let wrapped = (today + days_back).rem_euclid(7);
    println!("with %: {result}");
    println!("with rem_euclid: {wrapped}");
}
Output
with %: -1
with rem_euclid: 6

8. Overflow the compiler can see is a compile error

When the Rust compiler can see an overflow, it refuses to compile.

Rust✗ does not compile
fn main() {
    let max = i32::MAX;
    let next = max + 1;
    println!("{next}");
}
Output
error: this arithmetic operation will overflow
 --> src/main.rs:3:16
  |
3 |     let next = max + 1;
  |                ^^^^^^^ attempt to compute `i32::MAX + 1_i32`, which would overflow
  |
  = note: `#[deny(arithmetic_overflow)]` on by default

When an overflow only happens at runtime, debug builds stop the program with a panic (Rust's crash for bugs). You will see one in Control Flow. Release builds wrap around silently for speed, so do not rely on the panic: say what you want explicitly, as the next example shows.

9. Choosing overflow behavior explicitly

Integer types have methods for each overflow policy:

  • checked_* returns Some(result), or None if it would overflow. These are values of type Option, covered in Option; {:?} prints them.
  • wrapping_* wraps around.
  • saturating_* stops at the minimum or maximum.
  • overflowing_* returns the wrapped result and whether it overflowed.
Rust
fn main() {
    let volume: u8 = 250;
    println!("checked:     {:?}", volume.checked_add(10));
    println!("checked ok:  {:?}", volume.checked_add(5));
    println!("wrapping:    {}", volume.wrapping_add(10));
    println!("saturating:  {}", volume.saturating_add(10));
    println!("overflowing: {:?}", volume.overflowing_add(10));
}
Output
checked:     None
checked ok:  Some(255)
wrapping:    4
saturating:  255
overflowing: (4, true)

Use saturating_sub for counters that should stop at zero, and checked_* for money and anything where overflow means a bug you must handle.

10. Integer methods

Rust
fn main() {
    let temperature: i32 = -15;
    println!("abs: {}", temperature.abs());
    println!("pow: {}", 2_i32.pow(10));
    println!("min: {}", 7.min(3));
    println!("max: {}", 7.max(3));
    println!("clamp: {}", 150.clamp(0, 100));
    println!("signum: {}", temperature.signum());
    println!("is_power_of_two: {}", 64_u32.is_power_of_two());
    println!("count_ones: {}", 0b1011_u8.count_ones());
}
Output
abs: 15
pow: 1024
min: 3
max: 7
clamp: 100
signum: -1
is_power_of_two: true
count_ones: 3

Calling a method on a literal works: 7.min(3). When the literal's type matters, add a suffix, as in 2_i32.pow(10).

11. Floating-point numbers

f64 is the default and the right choice almost always. Floats follow the IEEE 754 standard.

Rust
fn main() {
    let a: f64 = 0.1;
    let b: f64 = 0.2;
    println!("0.1 + 0.2 = {}", a + b);
    println!("equal to 0.3? {}", a + b == 0.3);
    println!("close to 0.3? {}", (a + b - 0.3).abs() < 1e-10);

    let single: f32 = 1.0 / 3.0;
    let double: f64 = 1.0 / 3.0;
    println!("f32: {single}");
    println!("f64: {double}");
}
Output
0.1 + 0.2 = 0.30000000000000004
equal to 0.3? false
close to 0.3? true
f32: 0.33333334
f64: 0.3333333333333333

12. Float methods

Rust
fn main() {
    let x: f64 = 2.0;
    let price: f64 = 19.456;
    println!("sqrt: {}", x.sqrt());
    println!("powi: {}", x.powi(10));
    println!("powf: {}", x.powf(0.5));
    println!("round: {}", price.round());
    println!("floor: {}", price.floor());
    println!("ceil: {}", price.ceil());
    println!("trunc: {}", (-price).trunc());
    println!("cents: {}", (price * 100.0).round() / 100.0);
    println!("pi: {:.5}", std::f64::consts::PI);
}
Output
sqrt: 1.4142135623730951
powi: 1024
powf: 1.4142135623730951
round: 19
floor: 19
ceil: 20
trunc: -19
cents: 19.46
pi: 3.14159

std::f64::consts::PI is a constant in the standard library. The :: separates path segments.

13. Method calls need a known float type

Rust needs to know whether a float is f32 or f64 before it can call a method on it. If nothing tells it, you get an error. Fix it with a type annotation or a suffix such as 2.0_f64.

Rust✗ does not compile · E0689
fn main() {
    let x = 2.0;
    println!("{}", x.sqrt());
}
Output
error[E0689]: can't call method `sqrt` on ambiguous numeric type `{float}`
 --> src/main.rs:3:22
  |
3 |     println!("{}", x.sqrt());
  |                      ^^^^
  |
help: you must specify a type for this binding, like `f32`
  |
2 |     let x: f32 = 2.0;
  |          +++++

14. Special float values: infinity and NaN

Float division by zero does not crash. It produces infinity, or NaN ("not a number").

Rust
fn main() {
    let positive: f64 = 1.0 / 0.0;
    let negative: f64 = -1.0 / 0.0;
    let not_a_number = 0.0_f64 / 0.0;
    println!("{positive} {negative} {not_a_number}");
    println!("NaN == NaN? {}", not_a_number == not_a_number);
    println!("is_nan? {}", not_a_number.is_nan());
    println!("is_finite? {}", positive.is_finite());
}
Output
inf -inf NaN
NaN == NaN? false
is_nan? true
is_finite? false

Integer division by zero is different: it is a compile error if the compiler can see it, and a panic at runtime otherwise.

15. Booleans and logical operators

&&, ||, and ! work as logical operators and short-circuit.

Rust
fn main() {
    let is_admin = false;
    let is_owner = true;
    let is_banned = false;
    let can_edit = (is_admin || is_owner) && !is_banned;
    println!("can edit: {can_edit}");
    println!("as number: {}", can_edit as i32);
}
Output
can edit: true
as number: 1

16. No truthiness: conditions must be bool

Unlike C or JavaScript, a number is never treated as true or false.

Rust✗ does not compile · E0308
fn main() {
    let unread = 3;
    if unread {
        println!("You have mail");
    }
}
Output
error[E0308]: mismatched types
 --> src/main.rs:3:8
  |
3 |     if unread {
  |        ^^^^^^ expected `bool`, found integer

Write the comparison instead: if unread > 0.

17. Characters are full Unicode

A Rust char is 4 bytes and can hold any Unicode character, including emoji.

Rust
fn main() {
    let letter = 'a';
    let accented = 'é';
    let kanji = '字';
    let crab = '🦀';
    println!("{letter} {accented} {kanji} {crab}");
    println!("code point of crab: U+{:X}", crab as u32);
    println!("size of char: {} bytes", std::mem::size_of::<char>());
}
Output
a é 字 🦀
code point of crab: U+1F980
size of char: 4 bytes

std::mem::size_of::<char>() returns the size of a type in bytes. The ::<char> part names the type to measure; it is called the "turbofish" and appears again in Generics.

18. Character methods

Rust
fn main() {
    let c = 'q';
    let digit = '7';
    println!("{}", c.is_alphabetic());
    println!("{}", c.is_uppercase());
    println!("{}", c.to_ascii_uppercase());
    println!("{}", digit.is_ascii_digit());
    println!("{:?}", digit.to_digit(10));
    println!("{}", ' '.is_whitespace());
}
Output
true
false
Q
true
Some(7)
true

to_digit(10) returns Some(7) because the conversion can fail (for example on 'x'). This is an Option; see Option.

19. Bitwise operators

Bitwise NOT is ! (not ~), and there is no >>> operator. For a logical (unsigned) shift, use an unsigned type.

Rust
fn main() {
    let read: u8 = 0b100;
    let write: u8 = 0b010;
    let execute: u8 = 0b001;

    let permissions = read | write;            // combine flags
    let can_write = permissions & write != 0;  // test a flag
    let without_write = permissions & !write;  // clear a flag
    let toggled = permissions ^ execute;       // flip a flag

    println!("permissions:   {permissions:03b}");
    println!("can write:     {can_write}");
    println!("without write: {without_write:03b}");
    println!("toggled:       {toggled:03b}");
    println!("1 << 4 = {}", 1 << 4);
    println!("0xF0 >> 4 = {}", 0xF0 >> 4);
}
Output
permissions:   110
can write:     true
without write: 100
toggled:       111
1 << 4 = 16
0xF0 >> 4 = 15

20. No automatic widening between number types

Rust never widens numbers implicitly: every conversion is explicit.

Rust✗ does not compile · E0308
fn main() {
    let quantity: i32 = 3;
    let unit_price: i64 = 1_500;
    let total: i64 = quantity * unit_price;
    println!("{total}");
}
Output
error[E0308]: mismatched types
 --> src/main.rs:4:33
  |
4 |     let total: i64 = quantity * unit_price;
  |                                 ^^^^^^^^^^ expected `i32`, found `i64`

This feels strict at first, but it removes a whole family of bugs: a quiet conversion never loses data or changes a sign without you asking for it.

21. Converting with as

as converts between number types. Converting to a wider type is always safe. Converting to a narrower type or between signed and unsigned can change the value.

Rust
fn main() {
    let quantity: i32 = 3;
    let unit_price: i64 = 1_500;
    println!("total: {}", quantity as i64 * unit_price);

    println!("i32 -> f64: {}", 7 as f64 / 2.0);
    println!("f64 -> i32 truncates: {}", 9.99 as i32);
    println!("f64 -> i32 negative: {}", -9.99 as i32);
    let big: i32 = 300;
    println!("300 as u8 wraps: {}", big as u8);
    println!("-1 as u32: {}", -1_i32 as u32);
    println!("huge float saturates: {}", 1e20 as i32);
    println!("NaN becomes zero: {}", f64::NAN as i32);
}
Output
total: 4500
i32 -> f64: 3.5
f64 -> i32 truncates: 9
f64 -> i32 negative: -9
300 as u8 wraps: 44
-1 as u32: 4294967295
huge float saturates: 2147483647
NaN becomes zero: 0

Float-to-integer conversions saturate in Rust (1e20 as i32 gives i32::MAX), and NaN becomes 0.

22. Lossless conversion with From

When a conversion can never lose data, prefer i64::from(x) over x as i64. If someone later changes x to a type that could lose data, the code stops compiling instead of silently truncating. Conversion Traits covers this in depth.

Rust
fn main() {
    let small: u8 = 200;
    let medium: i32 = -40;
    let widened = u32::from(small);
    let as_i64 = i64::from(medium);
    let as_f64 = f64::from(medium);
    println!("{widened} {as_i64} {as_f64}");
}
Output
200 -40 -40

23. usize for sizes and indexes

Lengths and positions in Rust are usize, an unsigned integer as wide as a memory address (64 bits on today's machines). You will meet it constantly: len() returns usize, and array indexes must be usize.

Rust
fn main() {
    let name = "Rustacean";
    let length: usize = name.len();
    let half = length / 2;
    println!("{name} has {length} bytes; half is {half}");
    println!("usize max: {}", usize::MAX);
}
Output
Rustacean has 9 bytes; half is 4
usize max: 18446744073709551615

24. Realistic example: store money as integer cents

Floats cannot represent most decimal fractions exactly, so real billing systems store money as whole cents. This splits a bill fairly, giving the leftover cents to the first people.

Rust
fn main() {
    let bill_cents: i64 = 10_000; // 100.00
    let people: i64 = 3;

    let share = bill_cents / people;
    let leftover = bill_cents % people;

    println!("Each pays {}.{:02}", share / 100, share % 100);
    println!("Leftover cents: {leftover}");
    let first_share = share + 1;
    println!("The first {leftover} person(s) pay {}.{:02}", first_share / 100, first_share % 100);
}
Output
Each pays 33.33
Leftover cents: 1
The first 1 person(s) pay 33.34

25. Realistic example: human-readable file sizes

Rust
fn main() {
    let bytes: u64 = 5_368_709_120;
    let kib = bytes as f64 / 1024.0;
    let mib = kib / 1024.0;
    let gib = mib / 1024.0;
    println!("{bytes} bytes");
    println!("= {kib:.1} KiB");
    println!("= {mib:.1} MiB");
    println!("= {gib:.2} GiB");
}
Output
5368709120 bytes
= 5242880.0 KiB
= 5120.0 MiB
= 5.00 GiB

26. Realistic example: packing an RGB color into one integer

Graphics code often stores a color as one u32 in the form 0xRRGGBB. Shifts and masks pack and unpack it.

Rust
fn main() {
    let red: u8 = 255;
    let green: u8 = 128;
    let blue: u8 = 64;

    let packed: u32 = (u32::from(red) << 16) | (u32::from(green) << 8) | u32::from(blue);
    println!("packed: {packed:#08X}");

    let r = (packed >> 16) & 0xFF;
    let g = (packed >> 8) & 0xFF;
    let b = packed & 0xFF;
    println!("unpacked: r={r} g={g} b={b}");

    let gray = (r * 30 + g * 59 + b * 11) / 100;
    println!("grayscale: {gray}");
}
Output
packed: 0xFF8040
unpacked: r=255 g=128 b=64
grayscale: 159

27. Realistic example: body mass index

Rust
fn main() {
    let weight_kg: f64 = 72.5;
    let height_cm: u32 = 178;

    let height_m = f64::from(height_cm) / 100.0;
    let bmi = weight_kg / height_m.powi(2);
    let rounded = (bmi * 10.0).round() / 10.0;

    println!("Height: {height_m} m");
    println!("BMI: {rounded}");
    println!("Healthy range: {}", (18.5..25.0).contains(&bmi));
}
Output
Height: 1.78 m
BMI: 22.9
Healthy range: true

18.5..25.0 is a range, and contains(&bmi) checks whether bmi lies inside it. Ranges appear in Control Flow, and the & in References & Borrowing.

Summary

  • Integers: i8 to i128 (signed), u8 to u128 (unsigned), and isize/usize (pointer-sized). The default is i32.
  • Floats: f32 and f64. The default is f64.
  • Overflow is a compile error or a debug-mode panic, never silent in debug. Use checked_, wrapping_, and saturating_ methods to choose behavior.
  • Conditions must be bool.
  • char is a 4-byte Unicode scalar value, not a UTF-16 unit.
  • There are no automatic numeric conversions. Use T::from(x) for lossless conversions and as for everything else.

Next: Functions.