Scalar Types

Learn about Rust's four scalar data types - integers floating-point numbers booleans and characters - with type annotations ranges and how to choose the right type for your needs

A scalar type holds exactly one value at a time. It does not group, collect, or arrange multiple values. Rust has four built-in scalar types: integers, floating-point numbers, booleans, and characters. Each answers a different question about what kind of single piece of data a variable is allowed to hold, and the compiler enforces that answer at compile time.

Why Scalar Types Matter:

Rust needs to know every variable's type before your program runs. That knowledge lets it allocate exactly the right amount of memory, catch type mismatches early, and generate efficient machine code. Scalar types are the simplest expression of that guarantee.

Integer Types

An integer is a whole number without a fractional component. Rust's integer types differ along two axes: signed or unsigned, and bit-width. A signed integer can represent negative, zero, and positive values. An unsigned integer only represents zero and positive values, which doubles the maximum positive value it can hold for the same number of bits.

Rust provides these integer types:

LengthSignedUnsigned
8-biti8u8
16-biti16u16
32-biti32u32
64-biti64u64
128-biti128u128
archisizeusize

The isize and usize types depend on the architecture of the machine your program is compiled for: 32 bits on a 32-bit system, 64 bits on a 64-bit system. Their primary purpose is indexing into collections, where the pointer width determines how much addressable memory exists.

When you write let x = 42; without an explicit type annotation, Rust infers i32 — a 32-bit signed integer. That default covers a wide range of common values and balances performance with range.

let small: i8 = -120;           // signed, fits in 8 bits
let large: u64 = 18_446_744_073_709_551_615; // max u64
let idx: usize = 4;             // architecture-dependent

The second line uses an underscore as a visual separator. Rust ignores underscores in numeric literals, so 1_000_000 is the same as 1000000. This makes large numbers easier to scan without affecting the value.

Good Default Choice:

i32 is the default integer type and works well for most numeric tasks that don't involve indexing or tight memory constraints. You only need to reach for a different integer type when you know the value range or memory footprint matters.

Integer Literal Forms

Rust supports writing integer literals in several bases. The base prefix determines how the compiler interprets the digits that follow.

let decimal = 98_222;          // base 10
let hex = 0xff;                // base 16
let octal = 0o77;              // base 8
let binary = 0b1111_0000;      // base 2
let byte = b'A';               // u8 literal only

The byte literal b'A' is syntax for a u8 value. It only works with ASCII characters and provides the byte value of the character — 65 in this case. It is not a char; it is an unsigned 8-bit integer.

Literal Type is Not Explicit:

The literal 0xff does not specify its integer type. It is just a value that the compiler uses to infer the type from context. If you need a specific type for a literal, use a suffix like 0xffu8 or an explicit type annotation on the variable.

Integer Overflow Behavior

Each integer type has a fixed, finite range. If you perform arithmetic that pushes a value outside that range, Rust behaves differently depending on the compilation profile.

In debug mode (the default during development), integer overflow causes a panic — the program stops with an error message. This immediate feedback catches bugs early.

In release mode, overflow wraps around using two's complement arithmetic. The value cycles back to the minimum for negative overflow or to zero for positive overflow, and the program continues silently. This choice prioritizes performance but can hide logic errors.

// In debug mode, this panics at runtime.
// In release mode, `small` becomes -128 (wrapping).
let small: i8 = 127;
let overflowed = small + 1;

To explicitly handle overflow, the standard library provides methods like wrapping_add, checked_add, overflowing_add, and saturating_add. These let you choose the overflow behavior on a case-by-case basis regardless of the build profile.

Release-Mode Overflow Can Mask Bugs:

Wrapping arithmetic can produce values that are mathematically wrong but cause no immediate crash. If your program relies on values staying within a range, use checked_* methods or explicit assertions to catch overflows even in release builds.

Floating-Point Types

Floating-point numbers store values that have a decimal point. Rust provides f32 (32 bits) and f64 (64 bits), both following the IEEE 754 standard. The default inferred type is f64 because on modern processors it is roughly the same speed as f32 but offers more precision.

let pi = 3.141592653589793;     // f64
let e: f32 = 2.71828;           // f32

All floating-point types in Rust are signed; they can represent positive and negative numbers, as well as special values: positive and negative infinity, and NaN (Not a Number).

IEEE 754 Precision Is Not Exact:

Floating-point numbers are approximations. Values like 0.1 cannot be represented precisely in binary, so calculations accumulate small rounding errors. For exact decimal arithmetic, use the rust_decimal crate or integer-based fixed-point arithmetic.

Special Values and Comparisons

Operations that exceed the representable range produce infinity instead of panicking:

let inf = 1.0 / 0.0;     // f64::INFINITY
let neg_inf = -1.0 / 0.0; // f64::NEG_INFINITY

Any mathematical operation that has no meaningful result — such as 0.0 / 0.0 or (-1.0_f64).sqrt() — produces NaN. Crucially, NaN is not equal to any value, including itself. Comparing two NaN values with == always returns false.

let nan = f64::NAN;
assert_ne!(nan, nan); // passes: NaN is never equal to NaN

This property affects sorting, hashing, and any logic that assumes equality works normally. Rust's f32 and f64 implement neither Eq nor Ord because they cannot provide a total order over all values.

NaN Spreads Through Operations:

Any arithmetic involving NaN returns NaN. If you suspect your computation might produce NaN, use is_nan() to check before using the result in further logic.

The Boolean Type

The bool type represents exactly one of two values: true or false. It occupies one byte of memory, even though a single bit would theoretically suffice. This is because the smallest addressable unit in modern hardware is a byte.

let is_ready = true;
let is_done: bool = false;

Boolean values are most often the result of comparison and logical expressions, but they are also first-class values you can store, pass, and return.

No Truthy or Falsy Coercion:

Rust does not implicitly convert non-boolean values into booleans. An if condition must be an actual bool. This eliminates an entire category of bugs common in languages where 0, "", or nil are treated as false.

Characters

Rust's char type represents a single Unicode scalar value. It is four bytes wide and can hold any code point from U+0000 to U+10FFFF, excluding the surrogate range U+D800 to U+DFFF. This means char is not limited to ASCII; it handles accented letters, emoji, Chinese characters, and many other scripts.

let letter = 'A';
let digit = '7';
let emoji = '❤';
let japanese = '本';

A char literal is written with single quotes. Double quotes create a string literal, which is a different type entirely — &str. Confusing the two results in a compile error, not a subtle runtime bug.

let c: char = 'X';     // ok
// let s: char = "X";  // compile error: expected char, found &str

Surrogate Code Points Are Invalid:

Unicode surrogates (\u{D800} through \u{DFFF}) are not valid Unicode scalar values. Using them as a char literal causes a compile-time error. The Rust compiler enforces that a char is always a valid Unicode scalar.

How char Differs from an Integer

Although a char has a numeric Unicode code point, Rust treats it as a distinct type from integer types. You cannot do arithmetic directly on a char without converting it first. The standard library provides methods like char::from_u32() and the as cast for conversions, but using them means you are explicitly acknowledging that you are moving between a character and its numeric representation.

let heart = '❤';
let codepoint = heart as u32;
println!("Code point of ❤: {:#x}", codepoint);
// Output: Code point of ❤: 0x2764

This separation prevents accidentally treating characters as numbers and helps avoid subtle encoding mistakes.

Summary

The four scalar types — integers, floating-point numbers, booleans, and characters — give you a precise way to describe the shape of individual values in your program. Integers let you choose signedness and bit-width to match the range you need. Floating-point numbers handle approximate real numbers, at the cost of exactness. Booleans keep conditional logic explicit and clean. Characters store a single Unicode scalar value, not a byte and not a string.

A mental model that helps: scalar types are the atoms of Rust's type system. They are the smallest pieces of data you can name, and everything else — arrays, structs, enums — is built by combining them. The compiler's knowledge of these atoms is what lets Rust catch type errors at compile time rather than at runtime.