RustNumeric Types

Numeric Types in Rust — A Deep Dive

Numbers are at the heart of nearly every program. Rust gives you precise control over numeric representation, arithmetic behaviour, and conversions. This page goes beyond the basics to cover integer literals, float precision, numeric methods, checked arithmetic, string parsing, type casting, and special float values.

Integer Literal Formats

Rust supports several ways to write integer literals. All formats accept underscores as visual separators and can be followed by a type suffix.

Format

Prefix

Example

Decimal value

Decimal

(none)

1_000_000

1,000,000

Hexadecimal

0x

0xFF

255

Octal

0o

0o77

63

Binary

0b

0b1111_0000

240

Byte (u8 only)

b'

b'A'

65

RUST
fn main() {
    let decimal     = 1_000_000;
    let hex         = 0xFF;
    let octal       = 0o77;
    let binary      = 0b1111_0000;
    let byte: u8    = b'A';      // ASCII value of 'A'

    println!("decimal  = {}", decimal);
    println!("hex 0xFF = {}", hex);
    println!("octal    = {}", octal);
    println!("binary   = {}", binary);
    println!("byte b'A' = {}", byte);

    // Type suffix directly on the literal
    let x = 255u8;
    let y = -42i16;
    let z = 3_000_000u64;
    println!("suffixed: {} {} {}", x, y, z);
}
decimal  = 1000000
hex 0xFF = 255
octal    = 63
binary   = 240
byte b'A' = 65
suffixed: 255 -42 3000000
Floating-Point Types: f32 vs f64

Rust has two floating-point types following the IEEE 754 standard:

  • f64 — 64-bit double precision. The default. Use this unless you have a specific reason not to (memory-constrained embedded targets, SIMD, GPU interop).
  • f32 — 32-bit single precision. Roughly half the memory, less precision (~7 significant decimal digits vs ~15 for f64). Some graphics and ML APIs expect f32.

RUST
fn main() {
    let d: f64 = 1.0 / 3.0;
    let s: f32 = 1.0 / 3.0;

    println!("f64: {:.20}", d);  // more precise
    println!("f32: {:.20}", s);  // less precise — notice the difference

    // Default literal type
    let x = 2.5;          // f64
    let y: f32 = 2.5;     // must annotate for f32

    println!("x is f64: {}", x);
    println!("y is f32: {}", y);
}
f64: 0.33333333333333331000
f32: 0.33333334326744080000
x is f64: 2.5
y is f32: 2.5
Tip
When in doubt, use f64. It is the same speed as f32on most modern CPUs and avoids hard-to-debug precision surprises.
Integer Arithmetic and Division Truncation

Integer division in Rust truncates towards zero — the fractional part is simply discarded, not rounded. This catches many beginners off guard.

RUST
fn main() {
    println!("7 / 2 = {}", 7 / 2);     // 3, not 3.5
    println!("7 % 2 = {}", 7 % 2);     // 1 (remainder)
    println!("-7 / 2 = {}", -7 / 2);   // -3, truncates towards zero
    println!("-7 % 2 = {}", -7 % 2);   // -1 (sign follows dividend)

    // To get a float result, cast first
    let a = 7_f64;
    let b = 2_f64;
    println!("7.0 / 2.0 = {}", a / b); // 3.5
}
7 / 2 = 3
7 % 2 = 1
-7 / 2 = -3
-7 % 2 = -1
7.0 / 2.0 = 3.5
Useful Numeric Methods — Integers

RUST
fn main() {
    let n: i32 = -42;

    println!("abs:    {}", n.abs());           // 42
    println!("pow:    {}", 2_i32.pow(10));     // 1024
    println!("min:    {}", 10_i32.min(20));    // 10
    println!("max:    {}", 10_i32.max(20));    // 20
    println!("clamp:  {}", 150_i32.clamp(0, 100)); // 100

    // Count bits
    let x: u8 = 0b1010_1010;
    println!("bits set: {}", x.count_ones());  // 4
    println!("leading zeros: {}", x.leading_zeros()); // 0
}
abs:    42
pow:    1024
min:    10
max:    20
clamp:  100
bits set: 4
leading zeros: 0
Useful Numeric Methods — Floats

RUST
fn main() {
    let x: f64 = -3.7;
    let y: f64 = 16.0;

    println!("abs:    {}", x.abs());       // 3.7
    println!("sqrt:   {}", y.sqrt());      // 4.0
    println!("cbrt:   {}", 27.0_f64.cbrt()); // 3.0
    println!("powi:   {}", 2.0_f64.powi(8));  // 256.0  (integer exponent)
    println!("powf:   {}", 2.0_f64.powf(0.5)); // 1.414...

    println!("floor:  {}", x.floor());    // -4.0
    println!("ceil:   {}", x.ceil());     // -3.0
    println!("round:  {}", x.round());    // -4.0
    println!("trunc:  {}", x.trunc());    // -3.0  (towards zero)
    println!("fract:  {}", x.fract());    // -0.7  (fractional part)

    println!("min:    {}", x.min(0.0));   // -3.7
    println!("max:    {}", x.max(0.0));   // 0.0
    println!("clamp:  {}", x.clamp(-3.0, 3.0)); // -3.0

    println!("ln:     {:.4}", std::f64::consts::E.ln()); // 1.0000
    println!("log2:   {:.4}", 8.0_f64.log2());           // 3.0000
    println!("log10:  {:.4}", 1000.0_f64.log10());       // 3.0000
}
abs:    3.7
sqrt:   4.0
cbrt:   3.0
powi:   256.0
powf:   1.4142135623730951
floor:  -4.0
ceil:   -3.0
round:  -4.0
trunc:  -3.0
fract:  -0.7000000000000002
min:    -3.7
max:    0.0
clamp:  -3.0
ln:     1.0000
log2:   3.0000
log10:  3.0000
Standard Math Constants

Rust provides standard mathematical constants in std::f64::consts and std::f32::consts.

RUST
use std::f64::consts;

fn main() {
    println!("PI      = {:.10}", consts::PI);        // 3.1415926536
    println!("E       = {:.10}", consts::E);         // 2.7182818285
    println!("SQRT_2  = {:.10}", consts::SQRT_2);   // 1.4142135624
    println!("LN_2    = {:.10}", consts::LN_2);     // 0.6931471806
    println!("LOG2_E  = {:.10}", consts::LOG2_E);   // 1.4426950408
    println!("FRAC_1_PI = {:.10}", consts::FRAC_1_PI); // 0.3183098862

    // Circumference of a circle with radius 5
    let radius = 5.0_f64;
    let circumference = 2.0 * consts::PI * radius;
    println!("Circumference: {:.4}", circumference);
}
PI      = 3.1415926536
E       = 2.7182818285
SQRT_2  = 1.4142135624
LN_2    = 0.6931471806
LOG2_E  = 1.4426950408
FRAC_1_PI = 0.3183098862
Circumference: 31.4159
NaN and Infinity

Floating-point arithmetic can produce two special values: NaN (Not a Number) and infinity (positive or negative). Rust exposes these through constants and methods.

Warning
NaN is not equal to anything — including itself. Never use == NANto check for NaN; use .is_nan() instead.

RUST
fn main() {
    let pos_inf = f64::INFINITY;
    let neg_inf = f64::NEG_INFINITY;
    let nan     = f64::NAN;

    println!("INFINITY:      {}", pos_inf);
    println!("NEG_INFINITY:  {}", neg_inf);
    println!("NAN:           {}", nan);

    // Operations that produce special values
    println!("1.0 / 0.0  = {}", 1.0_f64 / 0.0);   // inf
    println!("-1.0 / 0.0 = {}", -1.0_f64 / 0.0);  // -inf
    println!("0.0 / 0.0  = {}", 0.0_f64 / 0.0);   // NaN
    println!("sqrt(-1)   = {}", (-1.0_f64).sqrt()); // NaN

    // Checks
    println!("is_nan:      {}", nan.is_nan());
    println!("is_infinite: {}", pos_inf.is_infinite());
    println!("is_finite:   {}", 42.0_f64.is_finite());

    // NaN != NaN (IEEE 754 rule)
    println!("NAN == NAN:  {}", nan == nan);   // false!
    println!("NAN.is_nan: {}", nan.is_nan());  // true
}
INFINITY:      inf
NEG_INFINITY:  -inf
NAN:           NaN
1.0 / 0.0  = inf
-1.0 / 0.0 = -inf
0.0 / 0.0  = NaN
sqrt(-1)   = NaN
is_nan:      true
is_infinite: true
is_finite:   true
NAN == NAN:  false
NAN.is_nan: true
Checked, Saturating, and Wrapping Arithmetic

When overflow is a real risk, Rust provides explicit methods that let you choose what happens instead of panicking or wrapping silently.

Method family

Returns

Behaviour on overflow

.checked_add()

Option<T>

None if overflow, Some(result) otherwise

.saturating_add()

T

Clamps to MIN or MAX

.wrapping_add()

T

Wraps around (two's complement)

.overflowing_add()

(T, bool)

Returns (wrapped_value, did_overflow)

RUST
fn main() {
    let big: u8 = 250;

    // checked — returns Option
    println!("checked +3:  {:?}", big.checked_add(3));   // None
    println!("checked +4:  {:?}", big.checked_add(4));   // None
    println!("checked +1:  {:?}", 10_u8.checked_add(1)); // Some(11)

    // saturating — clamps to type boundary
    println!("saturating +100: {}", big.saturating_add(100)); // 255
    println!("saturating sub:  {}", 0_u8.saturating_sub(1));  // 0

    // wrapping — two's complement wrap
    println!("wrapping +10: {}", big.wrapping_add(10));  // 4  (250+10=260, 260-256=4)

    // overflowing — value + flag
    let (val, overflowed) = big.overflowing_add(10);
    println!("overflowing: val={} overflowed={}", val, overflowed);
}
checked +3:  None
checked +4:  None
checked +1:  Some(11)
saturating +100: 255
saturating sub:  0
wrapping +10: 4
overflowing: val=4 overflowed=true
Parsing Numbers from Strings

It is very common to receive numeric input as a string — from the command line, a file, or a web request — and need to convert it to a number. The .parse() method returns a Result because the string might not be a valid number.

RUST
fn main() {
    // Turbofish syntax to specify the target type
    let n: i32 = "42".parse::<i32>().unwrap();
    println!("parsed i32: {}", n);

    // Via type annotation on the binding
    let f: f64 = "3.14".parse().unwrap();
    println!("parsed f64: {}", f);

    // Handling parse errors gracefully with match
    let input = "not_a_number";
    match input.parse::<i32>() {
        Ok(num) => println!("got: {}", num),
        Err(e)  => println!("parse error: {}", e),
    }

    // Using unwrap_or for a default
    let value: i32 = "abc".parse().unwrap_or(0);
    println!("with default: {}", value);

    // Trimming whitespace before parsing (very common)
    let raw = "  100  ";
    let trimmed: u32 = raw.trim().parse().expect("expected a number");
    println!("trimmed and parsed: {}", trimmed);
}
parsed i32: 42
parsed f64: 3.14
parse error: invalid digit found in string
with default: 0
trimmed and parsed: 100
Tip
Always .trim() user input before parsing — trailing newlines and spaces are the most common cause of parse failures.
Type Conversion with as

The as keyword converts between numeric types. It always succeeds (no runtime error) but may silently truncate or reinterpret bits, so you must understand what it does.

RUST
fn main() {
    // Widening — always safe
    let small: i8 = 100;
    let big: i64 = small as i64;
    println!("i8 -> i64: {}", big);

    // Narrowing — truncates high bits
    let wide: i32 = 300;
    let narrow = wide as u8;          // 300 - 256 = 44
    println!("300 as u8: {}", narrow);

    let neg: i32 = -1;
    let u = neg as u32;               // reinterprets as u32::MAX
    println!("-1 as u32: {}", u);

    // Float to int — truncates towards zero (not round!)
    let f = 3.99_f64;
    println!("3.99 as i32: {}", f as i32);   // 3

    let neg_f = -3.99_f64;
    println!("-3.99 as i32: {}", neg_f as i32); // -3

    // Rounding before cast
    println!("round then cast: {}", f.round() as i32); // 4

    // Large float to integer — saturates in Rust 1.45+
    let huge = 1e18_f64;
    println!("huge f64 as i32: {}", huge as i32); // i32::MAX (saturates)
}
i8 -> i64: 100
300 as u8: 44
-1 as u32: 4294967295
3.99 as i32: 3
-3.99 as i32: -3
round then cast: 4
huge f64 as i32: 2147483647
Warning
Casting a float to an integer truncates — it does not round. Use.round(), .floor(), or .ceil() first if you need a specific rounding mode before the cast.
Safe Conversions with From and Into

For conversions that are always safe (no truncation possible), Rust provides From and Into traits. These are preferable to as when widening, because they are explicit about being lossless.

RUST
fn main() {
    let byte: u8 = 200;

    // From::from — explicit lossless widening
    let wide = u32::from(byte);
    println!("u8 -> u32: {}", wide);

    // Into — syntactic sugar for From
    let also_wide: u64 = byte.into();
    println!("u8 -> u64: {}", also_wide);

    // i32 -> i64 is always safe
    let n: i32 = -100;
    let big: i64 = i64::from(n);
    println!("i32 -> i64: {}", big);

    // u32 -> u64 is always safe
    let m: u32 = 4_000_000_000;
    let bigger: u64 = m.into();
    println!("u32 -> u64: {}", bigger);
}
u8 -> u32: 200
u8 -> u64: 200
i32 -> i64: -100
u32 -> u64: 4000000000
Note
From and Into only exist for lossless conversions.u8 implements From<u8> for u16,u32, etc., but not the other direction — narrowing casts require as (or a checked method).
Practical Example — A Small Calculator

Putting it all together: parsing, arithmetic, float methods, and safe output.

RUST
fn main() {
    let inputs = ["10", "3", "7.5"];

    let a: f64 = inputs[0].parse().expect("first must be a number");
    let b: f64 = inputs[1].parse().expect("second must be a number");
    let c: f64 = inputs[2].parse().expect("third must be a number");

    println!("a = {}, b = {}, c = {}", a, b, c);
    println!("a + b = {}", a + b);
    println!("a - b = {}", a - b);
    println!("a * c = {}", a * c);
    println!("a / b = {:.4}", a / b);
    println!("a % b = {}", a % b);
    println!("sqrt(a) = {:.4}", a.sqrt());
    println!("a.powi(3) = {}", a.powi(3));
    println!("min(a,c) = {}", a.min(c));
    println!("max(a,c) = {}", a.max(c));
    println!("clamp(b, 0.0, 2.5) = {}", b.clamp(0.0, 2.5));

    // Integer division
    let ia = a as i64;
    let ib = b as i64;
    println!("integer div: {} / {} = {}", ia, ib, ia / ib);
    println!("integer rem: {} % {} = {}", ia, ib, ia % ib);
}
a = 10, b = 3, c = 7.5
a + b = 13
a - b = 7
a * c = 75
a / b = 3.3333
a % b = 1
sqrt(a) = 3.1623
a.powi(3) = 1000
min(a,c) = 7.5
max(a,c) = 10
clamp(b, 0.0, 2.5) = 2.5
integer div: 10 / 3 = 3
integer rem: 10 % 3 = 1
Success
Rust's numeric types give you full control: choose the right size, pick the right overflow strategy, use checked arithmetic when correctness is critical, and rely on the compiler to catch type mismatches before they become runtime bugs.