---
title: How to use num crate in Rust numeric types
summary: The `num` crate provides a unified set of numeric traits and types that extend Rust's standard library, allowing you to write generic code that works across integers, floats, and complex numbers.
lang: en
slug: how-to-use-num-crate-in-rust-numeric-types
published: 2026-04-17T16:39:40+00:00
updated: 2026-04-25T22:00:13+00:00
canonical: https://www.rustfaq.org/en/how-to-use-num-crate-in-rust-numeric-types/
---

# How to use num crate in Rust numeric types



## When standard numbers run out of room

You are writing a physics simulation. The velocity values grow until they hit `i64::MAX` and wrap around to negative infinity. Your simulation crashes. You switch to `f64`, but floating point precision starts dropping decimals in the twelfth significant figure. Your trajectory calculations drift. You need a number type that grows as large as your RAM allows, or you need to work with complex numbers for signal processing. Rust's built-in `i32`, `u64`, and `f64` types are fast and predictable, but they are fixed size. They refuse to expand.

The `num` ecosystem solves this without breaking Rust's safety guarantees or sacrificing the speed of primitive types. It splits the problem into two crates: `num-traits` provides the mathematical interfaces, and `num` provides the heavy lifting types like `BigInt` and `Complex`. You combine them to write code that works across any numeric representation, from a 32-bit integer to a thousand-digit arbitrary precision number.

## The trait system behind the math

Rust separates mathematical behavior from concrete types using traits. The standard library defines basic operations like `Add` and `Mul`, but it leaves higher-level concepts like `Zero`, `One`, `Pow`, and `ToPrimitive` to the ecosystem. `num-traits` fills that gap. It defines a consistent vocabulary for numeric operations that any type can implement.

Think of `num-traits` as the rulebook for arithmetic. It specifies what it means for a type to have a zero value, how to convert between number formats, or how to raise a value to a power. The `num` crate is the workshop that builds the actual calculators following those rules. `BigInt` implements the rulebook. `Complex` implements the rulebook. Even `i32` implements the rulebook. This separation keeps the standard library lean while giving you a unified way to write generic math.

When you constrain a function with `num-traits` bounds, you are telling the compiler to accept any type that promises to follow the arithmetic rules. The compiler then generates specialized machine code for each concrete type you actually use. This is monomorphization. It means generic math in Rust compiles down to the exact same assembly as hand-written primitive math. There is no virtual dispatch overhead. There is no runtime type checking. The abstraction is zero cost.

## Writing generic math that actually compiles

Generic numeric functions require explicit trait bounds. Rust will not guess that you want to add two unknown types. You must state the requirements clearly.

```rust
use num_traits::{Zero, One, ToPrimitive};

/// Calculates the sum of two numeric values using generic bounds.
fn calculate_sum<T>(a: T, b: T) -> T 
where
    T: Zero + One + std::ops::Add<Output = T>,
{
    // The compiler verifies T supports addition and returns T.
    // Zero and One are included here to demonstrate common bounds,
    // even though this specific function only uses Add.
    a + b
}

fn main() {
    // Monomorphization creates a specialized version for i32.
    let int_sum = calculate_sum(10i32, 5i32);
    
    // A separate specialized version is generated for f64.
    let float_sum = calculate_sum(10.5f64, 2.5f64);
    
    // ToPrimitive returns Option because conversion can fail.
    // This prevents silent truncation or overflow at runtime.
    if let Some(val) = int_sum.to_f64() {
        println!("Converted to f64: {}", val);
    }
}
```

The `where` clause is the contract. `T: Zero + One + std::ops::Add<Output = T>` means the type must provide a zero constant, a one constant, and an addition operator that returns the same type. When you call `calculate_sum(10i32, 5i32)`, the compiler substitutes `i32` for `T` and checks that `i32` implements all three traits. It passes. The compiler then emits direct CPU addition instructions. When you call it with `f64`, it repeats the process. The runtime sees only fast, concrete arithmetic.

Notice the `ToPrimitive` usage. Numeric conversion in Rust is explicit and fallible. `to_f64()` returns `Option<f64>` because converting a massive `BigInt` to a 64-bit float might lose precision or exceed the representable range. The compiler forces you to handle the `None` case. This design prevents silent data corruption. Trust the `Option` return type. It is saving you from floating point traps.

## Beyond 64 bits: arbitrary precision and complex numbers

Primitive types live in CPU registers. They are fast because the hardware supports them directly. `BigInt` and `BigUint` live on the heap. They store digits in a `Vec<u32>` or `Vec<u64>` and implement arithmetic by iterating over those digits. The tradeoff is clear: you gain unlimited size at the cost of allocation and loop overhead. You use them when the problem size exceeds hardware limits.

```rust
use num::BigInt;
use num::Complex;

/// Demonstrates arbitrary precision integers and complex arithmetic.
fn main() {
    // BigInt allocates on the heap. It stores digits in base 2^32.
    // Parsing from a string avoids literal size limits.
    let big_num = BigInt::parse_bytes(b"1000000000000000000000000000000", 10).unwrap();
    
    // Multiplication allocates a new Vec for the result.
    // The algorithm handles carry propagation across digit boundaries.
    let result = big_num.clone() * &big_num;
    println!("Big integer result: {}", result);

    // Complex stores real and imaginary parts as f64.
    // It implements standard arithmetic traits for vector-like math.
    let c1 = Complex::new(1.0, 2.0);
    let c2 = Complex::new(3.0, 4.0);
    
    // Addition combines real parts and imaginary parts separately.
    // No allocation occurs because Complex is a small stack struct.
    let c_sum = c1 + c2;
    println!("Complex sum: {}", c_sum);
}
```

`BigInt` parsing from strings is the standard entry point. Rust's integer literals are capped at `u128`, so you cannot write a 200-digit number directly in source code without a macro or crate extension. `parse_bytes` reads the decimal representation and builds the internal digit array. The `clone()` call before multiplication is necessary because `BigInt` does not implement `Copy`. Moving the first value would leave it unusable, and the multiplication trait expects references or owned values that can be consumed. The community convention is to use `.clone()` or `&` explicitly when reusing large numeric values. It signals intent and avoids borrow checker friction.

`Complex` behaves differently. It is a fixed-size struct containing two `f64` values. It fits entirely on the stack. Arithmetic operations are single-pass calculations with no heap allocation. You get mathematical convenience without the allocation tax. Keep `Complex` for signal processing, 2D transformations, and control theory. Reserve `BigInt` for cryptography, combinatorics, and financial calculations where precision matters more than nanoseconds.

## Where the compiler pushes back

Generic numeric code triggers specific compiler errors when trait bounds are incomplete or types mismatch. The compiler will reject code with `E0277` (trait bound not satisfied) if you forget to require `Add` or `Zero`. It will reject mixed-type arithmetic with `E0308` (mismatched types) because Rust does not perform implicit numeric coercion. You cannot add an `i32` to an `f64` without an explicit conversion.

```rust
use num_traits::Zero;

/// This function will fail to compile without the Add bound.
fn broken_math<T>(a: T, b: T) -> T 
where
    T: Zero, // Missing std::ops::Add<Output = T>
{
    // E0277: the trait bound `T: std::ops::Add` is not satisfied
    a + b 
}
```

The error message points directly to the missing trait. Add `std::ops::Add<Output = T>` to the `where` clause and the code compiles. When mixing types, use `num_traits::cast::ToPrimitive` or `Into` conversions explicitly. Implicit coercion is a source of subtle bugs in systems programming. Rust forces you to declare where precision might be lost.

Another common friction point is assuming `to_f64()` or `to_i32()` always succeeds. They return `Option`. Unwrapping them blindly will panic on overflow. Pattern match with `if let` or `match` to handle the bounds safely. Treat the `Option` as a contract. The numeric crate is telling you that the target type cannot represent the source value. Handle the `None` case gracefully, or clamp the value before conversion.

Convention aside: always import traits explicitly from `num_traits` rather than relying on `num` to re-export them. The `num` crate pulls in `num-traits` as a dependency, but explicit imports make your generic bounds readable and future-proof. If the crate structure changes, your code breaks at the import line, not deep inside a trait resolution error. `cargo fmt` will align the imports automatically. Do not fight the formatter. Focus on the bounds.

## Picking the right numeric tool

Use primitive types like `i32`, `u64`, and `f64` when performance matters and the value range fits within hardware limits. Use `num_traits` generic bounds when you are writing libraries or utilities that must accept multiple numeric types without duplicating logic. Use `BigInt` or `BigUint` when your calculations exceed 128 bits and precision is mandatory. Use `Complex` when you need real and imaginary components for mathematical modeling or signal processing. Use `nalgebra` or `faer` when you are working with matrices, vectors, and linear algebra operations at scale.

Primitive types compile to single CPU instructions. Generic bounds compile to specialized versions of those instructions. `BigInt` trades speed for unlimited range. `Complex` trades simplicity for mathematical completeness. Linear algebra crates trade abstraction for SIMD-optimized bulk operations. Match the tool to the constraint. Do not reach for `BigInt` in a tight game loop. Do not force `f64` into a cryptographic hash. Let the type system enforce the boundary.

## Where to go next

- [How to Do Linear Algebra in Rust (nalgebra, faer)](/en/how-to-do-linear-algebra-in-rust-nalgebra-faer/)
- [How to Use ndarray for Numerical Computing in Rust](/en/how-to-use-ndarray-for-numerical-computing-in-rust/)