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.
[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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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_distr crate documentation
In this article we have generated random numbers in Rust.
Author
List all Rust tutorials.