Rust Data Types

Floating-Point Types

Rust provides two primitive floating-point types for representing real numbers with fractional parts: f32 (32-bit single-precision) and f64 (64-bit double-precision). Both types conform to the IEEE 754-2008 standard for binary floating-point arithmetic (binary32 and binary64).

f32 and f64 Overview

When a floating-point literal such as 3.14 is written without a type suffix or external type constraint, Rust defaults to f64 because on modern 64-bit CPUs, f64 executes at virtually the same speed as f32 while offering more than double the decimal precision.

Property f32 (Single Precision) f64 (Double Precision)
Total size 32 bits (4 bytes) 64 bits (8 bytes)
Sign / Exponent / Fraction bits 1 / 8 / 23 bits 1 / 11 / 52 bits
Decimal digits of precision (DIGITS) ~6 to 9 decimal digits (f32::DIGITS == 6) ~15 to 17 decimal digits (f64::DIGITS == 15)
Largest finite value (MAX) ≈ 3.4028235 × 10³⁸ ≈ 1.7976931348623157 × 10³⁰⁸
Smallest positive normal (MIN_POSITIVE) ≈ 1.1754944 × 10⁻³⁸ ≈ 2.2250738585072014 × 10⁻³⁰⁸
Machine epsilon (EPSILON) ≈ 1.1920929 × 10⁻⁷ ($2^{-23}$) ≈ 2.220446049250313 × 10⁻¹⁶ ($2^{-52}$)
fn main() {
    let default_float = 2.5;       // Inferred as f64
    let single_prec: f32 = 2.5;    // Annotated as f32
    let suffixed = 2.5_f32;        // Suffixed literal

    println!("f64 pi: {:.15}", std::f64::consts::PI);
    println!("f32 pi: {:.15}", std::f32::consts::PI);
    println!("Values: {default_float}, {single_prec}, {suffixed}");
}
f64 pi: 3.141592653589793
f32 pi: 3.141592741012573
Values: 2.5, 2.5, 2.5

Notice that f32 loses accuracy after 7 decimal digits (3.1415927...), whereas f64 remains accurate across 15–17 decimal digits.

IEEE 754 Binary Layout

Every floating-point value is stored in scientific notation in base 2:

$$\text{value} = (-1)^{\text{sign}} \times (1.\text{fraction})_2 \times 2^{\text{exponent} - \text{bias}}$$

f32 (32 bits total):
+------+-------------------+-----------------------------------+
| Sign |     Exponent      |        Fraction (Mantissa)        |
| 1 b  | 8 bits (bias 127) |       23 bits (+1 implicit)       |
+------+-------------------+-----------------------------------+
  [31]        [30..23]                   [22..0]

f64 (64 bits total):
+------+--------------------+----------------------------------+
| Sign |      Exponent      |       Fraction (Mantissa)        |
| 1 b  | 11 bits (bias 1023)|      52 bits (+1 implicit)       |
+------+--------------------+----------------------------------+
  [63]        [62..52]                   [51..0]

Floating-Point Literals

A floating-point literal can be written with a decimal point, scientific exponent notation (e or E), visual underscores (_), or a type suffix (f32 or f64):

fn main() {
    let standard = 3.14159;
    let trailing_dot = 5.;           // Valid f64 literal (5.0), though `5.0` is preferred
    let scientific = 6.022e23_f64;   // 6.022 × 10²³
    let micro = 2.5e-6;              // 0.0000025
    let integer_with_suffix = 42f32; // Integer digits with `f32` suffix -> 42.0_f32
    let readable = 1_000_000.000_1;

    println!("{standard}, {trailing_dot}, {scientific:e}, {micro}, {integer_with_suffix}, {readable}");
}
3.14159, 5, 6.022e23, 0.0000025, 42, 1000000.0001

Note: While 5. is a valid float literal (5.0), calling a method directly on 5. (such as 5..sqrt()) is parsed as the range operator .. (5 .. sqrt()). Always write 5.0.sqrt() or 5_f64.sqrt().

Binary Representation Rounding and Equality

Many everyday base-10 fractions—such as 0.1, 0.2, and 0.3—are repeating fractions in base 2 ($\frac{1}{10} = 0.0001100110011\ldots_2$). Because the mantissa has a finite number of bits, these numbers are rounded to the nearest representable binary fraction.

As a result, 0.1 + 0.2 does not equal 0.3 bit-for-bit:

fn main() {
    let sum: f64 = 0.1 + 0.2;
    let target: f64 = 0.3;

    println!("0.1 + 0.2 = {sum:.17}");
    println!("0.3       = {target:.17}");
    println!("Direct == : {}", sum == target);

    // Compare using an absolute tolerance instead of `==`
    let close_enough = (sum - target).abs() < 1e-12;
    println!("Within 1e-12: {close_enough}");
}
0.1 + 0.2 = 0.30000000000000004
0.3       = 0.29999999999999999
Direct == : false
Within 1e-12: true

For financial calculations where exact base-10 arithmetic is required, represent amounts as integer smallest-currency units (such as cents in i64) or use a dedicated decimal library rather than binary floating-point types.

Special IEEE 754 Values: INFINITY, -0.0, and NaN

Unlike integer arithmetic—where dividing by zero panics—floating-point arithmetic never panics on overflow or division by zero. Instead, it produces special IEEE 754 values:

Expression Result Inspection Method
1.0 / 0.0 or f64::MAX * 2.0 inf (f64::INFINITY) .is_infinite() / .is_finite()
-1.0 / 0.0 -inf (f64::NEG_INFINITY) .is_infinite() && .is_sign_negative()
0.0 / 0.0 or (-1.0_f64).sqrt() NaN (f64::NAN, "Not a Number") .is_nan()
-1.0 / f64::INFINITY -0.0 (Negative zero) .is_sign_negative()
fn main() {
    let pos_inf = 1.0_f64 / 0.0;
    let neg_inf = -1.0_f64 / 0.0;
    let nan = 0.0_f64 / 0.0;
    let neg_zero = -0.0_f64;

    println!("pos_inf:  {pos_inf} (is_infinite: {})", pos_inf.is_infinite());
    println!("neg_inf:  {neg_inf} (is_finite:   {})", neg_inf.is_finite());
    println!("nan:      {nan} (is_nan:      {})", nan.is_nan());
    println!("-0.0 == 0.0: {} (neg_zero.is_sign_negative: {})", neg_zero == 0.0, neg_zero.is_sign_negative());
}
pos_inf:  inf (is_infinite: true)
neg_inf:  -inf (is_finite:   false)
nan:      NaN (is_nan:      true)
-0.0 == 0.0: true (neg_zero.is_sign_negative: true)

NaN Comparisons and total_cmp

According to IEEE 754, NaN is unordered and not equal to anything, including itself (f64::NAN == f64::NAN is false). Never test for NaN with x == f64::NAN; always call x.is_nan().

Because NaN != NaN violates the reflexivity law of total equality (a == a), f32 and f64 implement PartialEq and PartialOrd, but do not implement Eq or Ord. Consequently, calling .sort() on a slice of floats does not compile; use .sort_by(f64::total_cmp) instead:

fn main() {
    let mut measurements: [f64; 5] = [3.5, f64::NAN, -1.2, 0.0, f64::INFINITY];

    // `total_cmp` defines a total ordering: -NaN < -inf < ... < -0.0 < +0.0 < ... < +inf < +NaN
    measurements.sort_by(f64::total_cmp);

    println!("Sorted: {measurements:?}");
}
Sorted: [-1.2, 0.0, 3.5, inf, NaN]

Built-In Mathematical Methods

f32 and f64 provide rich mathematical methods directly on primitive values, and mathematical constants live in std::f32::consts and std::f64::consts:

fn main() {
    let x: f64 = 3.75;

    println!("floor: {}, ceil: {}, round: {}, trunc: {}", x.floor(), x.ceil(), x.round(), x.trunc());
    println!("sqrt(16.0): {}, cbrt(27.0): {}", 16.0_f64.sqrt(), 27.0_f64.cbrt());
    println!("2.0^10:     {}, hypot(3, 4): {}", 2.0_f64.powi(10), 3.0_f64.hypot(4.0));
    println!("clamp:      {}", x.clamp(0.0, 2.5));
}
floor: 3, ceil: 4, round: 4, trunc: 3
sqrt(16.0): 4, cbrt(27.0): 3
2.0^10:     1024, hypot(3, 4): 5
clamp:      2.5

Common Errors

Mixing Integer Literals or f32 with f64

Rust does not implicitly convert integers to floats—even integer literals—nor does it implicitly widen f32 to f64:

// ❌ does not compile
fn main() {
    let radius: f64 = 5;
    let area = 3.14_f32 * radius;
}
error[E0308]: mismatched types
 --> src/main.rs:2:23
  |
2 |     let radius: f64 = 5;
  |                 ---   ^ expected `f64`, found integer
  |                 |
  |                 expected due to this
  |
help: use a float literal
  |
2 |     let radius: f64 = 5.0;
  |                        ++

error[E0277]: cannot multiply `f32` by `f64`
 --> src/main.rs:3:25
  |
3 |     let area = 3.14_f32 * radius;
  |                         ^ no implementation for `f32 * f64`

error: aborting due to 2 previous errors

Write 5.0 for float literals, and convert explicitly (f64::from(single_prec) or int_val as f64) when combining types.