ZetCode

Rust random numbers

last modified October 25, 2026

In this article we show how to generate random numbers in Rust with the rand crate.

A random number generator (RNG) produces a sequence of values that has no recognizable pattern. Computer programs mostly use pseudo-random generators: deterministic algorithms that start from an initial value called the seed. With the same seed, a generator produces the same sequence. Random numbers are used in games, simulations, sampling, testing, and cryptography.

The Rust standard library does not have a public random number API. The de facto standard is the rand crate. Its default generator, ThreadRng, is a cryptographically secure generator that is seeded from the operating system and kept per thread.

The examples were tested with Rust 1.97, the 2024 edition, rand 0.10.3, and rand_distr 0.6.0. They are run as Cargo projects with cargo run -q. Since the values are random, the output differs on each run.

The rand crate

We add the dependencies with cargo add rand rand_distr. The rand_distr crate is needed only for the normal distribution example.

Cargo.toml
[dependencies]
rand = "0.10.3"
rand_distr = "0.6.0"

The [dependencies] section of the Cargo.toml file lists the two crates. In version 0.10, several items were renamed: the thread_rng function is now rand::rng, the gen and gen_range methods are random and random_range, and the methods are provided by the RngExt trait. Code written for rand 0.8 does not compile with the current version.

Random values of basic types

The rand::random function returns a random value of the type that is inferred from the context.

main.rs
fn main() {
    let n: u8 = rand::random();
    let x: f64 = rand::random();
    let b: bool = rand::random();

    println!("u8: {n}");
    println!("f64: {x}");
    println!("bool: {b}");
}

We generate a random byte, a random floating point number, and a random boolean value.

let n: u8 = rand::random();
let x: f64 = rand::random();

The type annotation decides what is generated. Integers cover the whole range of the type; floating point numbers are taken from the half-open range [0, 1). Booleans are true or false with equal probability.

$ cargo run -q
u8: 210
f64: 0.2594496853984233
bool: true
$ cargo run -q
u8: 146
f64: 0.9850020069513221
bool: false

Random numbers in a range

The rand::random_range function returns a random number from the given range.

main.rs
fn main() {
    let n = rand::random_range(1..=100);
    let die = rand::random_range(1..=6);
    let temp = rand::random_range(-10.0..35.0);

    println!("number: {n}");
    println!("die: {die}");
    println!("temperature: {temp:.1}");
}

The range can be exclusive (a..b) or inclusive (a..=b), and it works for integers, floats, and characters.

let die = rand::random_range(1..=6);

A throw of a die is a number from one to six inclusive. Each value has the same probability; the generated numbers are uniformly distributed.

let temp = rand::random_range(-10.0..35.0);

Floating point ranges work in the same way. The value is printed with one decimal place.

$ cargo run -q
number: 84
die: 4
temperature: -0.6
$ cargo run -q
number: 47
die: 3
temperature: -6.4
$ cargo run -q
number: 8
die: 6
temperature: -8.0

The thread-local generator

The rand::random and rand::random_range functions fetch the thread-local generator on each call. When we generate many values, we get the generator once with rand::rng and call its methods.

main.rs
use rand::RngExt;

fn main() {
    let mut rng = rand::rng();

    let vals: Vec<u32> = (0..8).map(|_| rng.random_range(0..10)).collect();
    println!("{vals:?}");

    let sum: u32 = (0..1000).map(|_| rng.random_range(0..10)).sum();
    println!("average of 1000 values: {}", sum as f64 / 1000.0);
}

The methods random, random_range, random_bool, sample, and sample_iter are defined in the RngExt trait, so the trait must be in scope.

let mut rng = rand::rng();

The rand::rng function returns a handle to the ThreadRng generator of the current thread. Generating a value changes the state of the generator, therefore the variable is mutable.

let sum: u32 = (0..1000).map(|_| rng.random_range(0..10)).sum();

We generate a thousand numbers from 0 to 9 and compute their average. With many values, the average gets close to the expected value of 4.5.

$ cargo run -q
[8, 4, 8, 7, 5, 7, 3, 4]
average of 1000 values: 4.586

Random booleans

The random_bool function returns true with the given probability; random_ratio takes the probability as a fraction.

main.rs
fn main() {
    let tries = 10_000;

    let heads = (0..tries).filter(|_| rand::random_bool(0.5)).count();
    let hits = (0..tries).filter(|_| rand::random_ratio(1, 6)).count();

    println!("heads: {heads} of {tries}");
    println!("one in six: {hits} of {tries}");
}

We simulate ten thousand coin tosses and count how many times an event with the probability of one in six occurs.

let heads = (0..tries).filter(|_| rand::random_bool(0.5)).count();

The probability passed to random_bool must be between 0 and 1; 0.5 is a fair coin.

let hits = (0..tries).filter(|_| rand::random_ratio(1, 6)).count();

The random_ratio(1, 6) call returns true in one case out of six. We expect about 1667 hits.

$ cargo run -q
heads: 4996 of 10000
one in six: 1697 of 10000

Seeded generators

A generator created from a fixed seed produces the same sequence every time the program runs. This is useful in tests, simulations, and procedural generation, where the results must be reproducible.

main.rs
use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};

fn main() {
    let mut rng = StdRng::seed_from_u64(42);

    let vals: Vec<u32> = (0..6).map(|_| rng.random_range(1..=49)).collect();
    println!("{vals:?}");

    let mut rng = StdRng::seed_from_u64(42);
    let first = rng.random_range(1..=49);
    println!("first value again: {first}");
}

We create a StdRng generator from a seed and generate six numbers from 1 to 49.

use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};

The seed_from_u64 function comes from the SeedableRng trait.

let mut rng = StdRng::seed_from_u64(42);

The generator is seeded with the value 42. Creating a second generator with the same seed starts the same sequence again, so the first value is the same.

The StdRng algorithm may change in a future version of rand. If the sequence must stay the same across versions, use a named algorithm, for example ChaCha8Rng from the chacha20 crate. Seeded generators must not be used for secrets; for these, use the ThreadRng or the operating system generator.

$ cargo run -q
[7, 26, 13, 27, 43, 32]
first value again: 7
$ cargo run -q
[7, 26, 13, 27, 43, 32]
first value again: 7

Filling a buffer

The rand::fill function fills a slice with random values.

main.rs
fn main() {
    let mut bytes = [0u8; 16];
    rand::fill(&mut bytes);

    let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();

    println!("{bytes:?}");
    println!("{hex}");
}

We fill an array of sixteen bytes with random data and print it as a hexadecimal string. Random bytes are used for tokens, salts, and identifiers.

let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();

Each byte is formatted as two hexadecimal digits and the pieces are collected into a String.

$ cargo run -q
[134, 215, 230, 191, 101, 89, 230, 111, 96, 157, 163, 220, 204, 242, 97, 145]
86d7e6bf6559e66f609da3dcccf26191

Iterators of random values

The random_iter function and the sample_iter method return infinite iterators of random values. A Uniform distribution is useful when we take many values from the same range.

main.rs
use rand::RngExt;
use rand::distr::Uniform;

fn main() {
    let vals: Vec<u16> = rand::random_iter().take(5).collect();
    println!("{vals:?}");

    let die = Uniform::new_inclusive(1, 6).unwrap();
    let rolls: Vec<u8> = rand::rng().sample_iter(die).take(20).collect();
    println!("{rolls:?}");

    let mut counts = [0; 6];
    for roll in rand::rng().sample_iter(die).take(60_000) {
        counts[roll as usize - 1] += 1;
    }
    println!("{counts:?}");
}

We generate five random u16 values, twenty die throws, and then count the results of sixty thousand throws.

let vals: Vec<u16> = rand::random_iter().take(5).collect();

The iterator never ends, so we limit it with take.

let die = Uniform::new_inclusive(1, 6).unwrap();
let rolls: Vec<u8> = rand::rng().sample_iter(die).take(20).collect();

The Uniform::new_inclusive function returns an error for an empty range, so the result is unwrapped. The distribution prepares the range once, which is faster than calling random_range repeatedly.

for roll in rand::rng().sample_iter(die).take(60_000) {
    counts[roll as usize - 1] += 1;
}

Each of the six values appears about ten thousand times.

$ cargo run -q
[46392, 64588, 17721, 24974, 3997]
[3, 6, 3, 1, 2, 2, 3, 3, 5, 2, 1, 2, 5, 5, 5, 4, 3, 5, 1, 3]
[10167, 10025, 10075, 9869, 9961, 9903]

Shuffling

The shuffle method from the SliceRandom trait randomly reorders the elements of a slice in place.

main.rs
use rand::seq::SliceRandom;

fn main() {
    let mut cards: Vec<u32> = (1..=10).collect();

    cards.shuffle(&mut rand::rng());
    println!("{cards:?}");

    cards.sort();
    println!("{cards:?}");
}

We shuffle the numbers from 1 to 10, like a deck of cards, and then sort them back.

cards.shuffle(&mut rand::rng());

The method takes a mutable reference to a generator. Every permutation has the same probability.

$ cargo run -q
[2, 10, 1, 3, 8, 9, 7, 5, 6, 4]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
$ cargo run -q
[2, 3, 6, 5, 7, 9, 10, 4, 1, 8]
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

Choosing random elements

The IndexedRandom trait adds the choose and sample methods to slices, arrays, and vectors.

main.rs
use rand::seq::IndexedRandom;

fn main() {
    let words = ["sky", "forest", "river", "cloud", "storm", "falcon", "rock"];
    let mut rng = rand::rng();

    if let Some(word) = words.choose(&mut rng) {
        println!("random word: {word}");
    }

    let picked: Vec<&str> = words.sample(&mut rng, 3).copied().collect();
    println!("three distinct words: {picked:?}");

    let empty: [i32; 0] = [];
    println!("from an empty slice: {:?}", empty.choose(&mut rng));
}

We pick one random word, then three distinct words.

if let Some(word) = words.choose(&mut rng) {
    println!("random word: {word}");
}

The choose method returns an Option, because an empty slice has no element to choose.

let picked: Vec<&str> = words.sample(&mut rng, 3).copied().collect();

The sample method returns an iterator over the given number of distinct elements, chosen without repetition. The iterator yields references to the elements; copied turns the &&str items into &str.

$ cargo run -q
random word: river
three distinct words: ["storm", "cloud", "rock"]
from an empty slice: None

Weighted choices

Sometimes the values should not have the same probability. The WeightedIndex distribution returns indexes with probabilities given by weights.

main.rs
use rand::RngExt;
use rand::distr::weighted::WeightedIndex;
use rand::seq::IndexedRandom;

fn main() {
    let names = ["common", "rare", "legendary"];
    let weights = [70, 25, 5];

    let dist = WeightedIndex::new(weights).unwrap();
    let mut counts = [0; 3];

    for idx in rand::rng().sample_iter(&dist).take(1000) {
        counts[idx] += 1;
    }

    for i in 0..names.len() {
        println!("{:<10} weight {:>2}: {}", names[i], weights[i], counts[i]);
    }

    let colors = [("red", 1), ("green", 2), ("blue", 7)];
    let (color, _) = colors.choose_weighted(&mut rand::rng(), |c| c.1).unwrap();
    println!("picked color: {color}");
}

We simulate a thousand loot drops, where common items have the weight 70, rare items 25, and legendary items 5.

let dist = WeightedIndex::new(weights).unwrap();

The distribution is created from the weights. It returns an error if the weights are empty, negative, or all zero.

let (color, _) = colors.choose_weighted(&mut rand::rng(), |c| c.1).unwrap();

For a single choice, the choose_weighted method is simpler. The closure returns the weight of an element. The blue color is seven times more likely than red.

$ cargo run -q
common     weight 70: 713
rare       weight 25: 242
legendary  weight  5: 45
picked color: blue

Random strings

Random strings are created from random characters. The Alphanumeric distribution generates ASCII letters and digits.

main.rs
use rand::RngExt;
use rand::distr::{Alphanumeric, SampleString};
use rand::seq::IndexedRandom;

fn main() {
    let mut rng = rand::rng();

    let id = Alphanumeric.sample_string(&mut rng, 12);
    println!("id: {id}");

    let code: String = (0..6).map(|_| rng.random_range('A'..='Z')).collect();
    println!("code: {code}");

    let charset: Vec<char> = "abcdefghjkmnpqrstuvwxyz23456789!#%&".chars().collect();
    let password: String = (0..16).map(|_| *charset.choose(&mut rng).unwrap()).collect();
    println!("password: {password}");
}

We generate an identifier, a code from uppercase letters, and a password from a custom set of characters.

let id = Alphanumeric.sample_string(&mut rng, 12);

The sample_string method from the SampleString trait returns a String of the given length.

let code: String = (0..6).map(|_| rng.random_range('A'..='Z')).collect();

Ranges of characters work with random_range too. The characters are collected into a string.

let charset: Vec<char> = "abcdefghjkmnpqrstuvwxyz23456789!#%&".chars().collect();
let password: String = (0..16).map(|_| *charset.choose(&mut rng).unwrap()).collect();

For a password, we choose characters from our own set. The set leaves out characters that are easy to confuse, such as l, 1, o, and 0. The ThreadRng generator is cryptographically secure, so it is suitable for passwords and tokens.

$ cargo run -q
id: pNJ2qulRLcn6
code: UHWHBY
password: fuczk6caed!kzk3f

The normal distribution

Many quantities, such as heights or measurement errors, follow the normal distribution: values near the mean are common and values far from it are rare. Non-uniform distributions are in the rand_distr crate.

main.rs
use rand::RngExt;
use rand_distr::{Distribution, Normal};

fn main() {
    let normal = Normal::new(170.0, 8.0).unwrap();
    let mut rng = rand::rng();

    let heights: Vec<f64> = normal.sample_iter(&mut rng).take(10_000).collect();

    let mean = heights.iter().sum::<f64>() / heights.len() as f64;
    let var = heights.iter().map(|h| (h - mean).powi(2)).sum::<f64>() / heights.len() as f64;
    let within = heights.iter().filter(|h| (*h - mean).abs() <= 8.0).count();

    println!("first five: {:.1?}", &heights[..5]);
    println!("mean: {mean:.2}");
    println!("standard deviation: {:.2}", var.sqrt());
    println!("within one deviation: {:.1} %", within as f64 / 100.0);

    let n: f64 = rng.sample(normal);
    println!("one more value: {n:.1}");
}

We generate ten thousand heights with the mean of 170 cm and the standard deviation of 8 cm and check their statistics.

let normal = Normal::new(170.0, 8.0).unwrap();

The Normal::new function takes the mean and the standard deviation. It returns an error if the deviation is negative or not finite.

let heights: Vec<f64> = normal.sample_iter(&mut rng).take(10_000).collect();

The sample_iter method of the Distribution trait takes the generator and returns an infinite iterator.

let within = heights.iter().filter(|h| (*h - mean).abs() <= 8.0).count();

About 68 percent of the values of a normal distribution lie within one standard deviation of the mean. The {:.1?} format prints the floats of the slice with one decimal place.

$ cargo run -q
first five: [166.6, 166.1, 180.0, 172.7, 160.6]
mean: 169.97
standard deviation: 7.99
within one deviation: 67.5 %
one more value: 164.8

Estimating pi

The Monte Carlo method uses random sampling to compute numerical results. We estimate the value of pi by generating random points in a unit square.

main.rs
use rand::RngExt;

fn main() {
    let mut rng = rand::rng();
    let n = 1_000_000;

    let inside = (0..n)
        .filter(|_| {
            let x: f64 = rng.random();
            let y: f64 = rng.random();
            x * x + y * y <= 1.0
        })
        .count();

    let pi = 4.0 * inside as f64 / n as f64;
    println!("estimate: {pi}");
    println!("error: {:.5}", (pi - std::f64::consts::PI).abs());
}

The quarter of a circle with the radius 1 has the area of pi/4. The ratio of the points that fall inside the circle to all points approaches this value.

let x: f64 = rng.random();
let y: f64 = rng.random();
x * x + y * y <= 1.0

A point is inside the circle if its distance from the origin is at most 1. We use a million points; the error of the estimate decreases slowly, with the square root of the number of points.

$ cargo run -q
estimate: 3.14568
error: 0.00409

Source

The rand crate documentation

The Rust Rand Book

The rand_distr crate documentation

In this article we have generated random numbers in Rust.

Author

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Rust tutorials.