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 |
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.
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
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.
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
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
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.
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.
== NANto check for NaN; use .is_nan() instead.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) |
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.
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
.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.
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
.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.
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
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.
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