Skip to content

Rust Random Numbers with rand

Games roll dice. Quizzes shuffle their questions. Tests need sample data. All of these need random numbers, and Rust’s standard library doesn’t have a ready-made random number generator. Instead, almost everyone uses one crate: rand.

This page shows the modern rand 0.10 API. That matters, because rand changed some names in versions 0.9 and 0.10. If an older tutorial shows thread_rng() or gen_range(), it was written for an older version. We’ll see the new names and the errors you get from the old ones.

In your project folder, run:

Terminal window
cargo add rand

That adds a line like this to your Cargo.toml:

[dependencies]
rand = "0.10"

If crates and Cargo.toml are new to you, read that page first. The “Try it” buttons on this page open the Rust Playground, which already includes rand.

For a one-off random value, use the free functions rand::random_range and rand::random:

fn main() {
let die: u32 = rand::random_range(1..=6);
let coin: bool = rand::random();
let chance: f64 = rand::random();
println!("die is between 1 and 6: {}", (1..=6).contains(&die));
println!("chance is in 0.0..1.0: {}", (0.0..1.0).contains(&chance));
println!("coin is heads: {coin}");
}
▶ Try it Yourself

There’s no output block here on purpose: the last line is true or false at random. Click “Try it” a few times and watch it change.

  • random_range(1..=6) gives a number from 1 to 6, including 6. With 1..6 you’d only get 1 to 5. Ranges work the same as in a for loop.
  • random() picks a value of whatever type you ask for. For a bool that’s a fair coin flip. For an f64 it’s a number from 0.0 up to (but not including) 1.0.
  • The type annotation (: u32, : bool) tells rand what to make. Without it, Rust may not know which type you want.

When you need many random values, grab a generator once with rand::rng() and call methods on it. This is a bit faster, and it’s the style you’ll see in most code:

use rand::RngExt;
fn main() {
let mut rng = rand::rng();
let rolls: Vec<u32> = (0..1000).map(|_| rng.random_range(1..=6)).collect();
println!("rolled {} dice", rolls.len());
println!("all between 1 and 6: {}", rolls.iter().all(|r| (1..=6).contains(r)));
let x: f64 = rng.random();
println!("x is in 0.0..1.0: {}", (0.0..1.0).contains(&x));
}

Output:

rolled 1000 dice
all between 1 and 6: true
x is in 0.0..1.0: true
▶ Try it Yourself

We print checks instead of the numbers themselves, so the output is the same every time even though the values aren’t.

Two more methods you’ll use often:

let lucky = rng.random_bool(0.1); // true about 10% of the time
let rare = rng.random_ratio(1, 100); // true about 1 time in 100

Why the use rand::RngExt; line? Methods like random_range and random_bool come from a trait, and you can only call a trait’s methods when the trait is in scope. In rand 0.10 there are two traits:

  • Rng is the basic “I can produce random bits” trait. Every generator implements it.
  • RngExt adds the friendly methods: random, random_range, random_bool and more. It works on every Rng automatically.

So: import RngExt to call the methods, and use Rng when you write a function that accepts “any generator”. You’ll see both in the dice roller below.

rand::rng() gives different numbers every run. That’s what you want in a real game. But for tests, debugging, or a game’s “daily challenge” level, you want the same “random” numbers every time.

A seed does this. A seeded generator always produces the same sequence for the same seed:

use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
fn main() {
let mut a = StdRng::seed_from_u64(2024);
let mut b = StdRng::seed_from_u64(2024);
let first: Vec<u8> = (0..4).map(|_| a.random_range(0..100)).collect();
let second: Vec<u8> = (0..4).map(|_| b.random_range(0..100)).collect();
println!("{:?}", first);
println!("{:?}", second);
println!("same? {}", first == second);
}

Output:

[53, 82, 74, 51]
[53, 82, 74, 51]
same? true
▶ Try it Yourself

seed_from_u64 comes from the SeedableRng trait, so that needs importing too. Change 2024 to another number and you get a different sequence, but still the same one on every run.

One honest warning: StdRng’s algorithm is allowed to change in a future rand release. The numbers above come from rand 0.10. If your tests must give the exact same numbers forever, the rand book recommends a named generator such as ChaCha8Rng (see “Read more” below).

To work with lists, import two more traits from rand::seq:

  • SliceRandom gives shuffle, which mixes up a slice in place.
  • IndexedRandom gives choose (one random item) and sample (several different items).
use rand::rngs::StdRng;
use rand::seq::{IndexedRandom, SliceRandom};
use rand::SeedableRng;
fn main() {
let mut rng = StdRng::seed_from_u64(7);
let mut cards = vec!["A", "K", "Q", "J", "10"];
cards.shuffle(&mut rng);
println!("shuffled: {:?}", cards);
let colors = ["red", "green", "blue"];
println!("picked: {:?}", colors.choose(&mut rng));
let team: Vec<&str> = cards.sample(&mut rng, 2).copied().collect();
println!("team: {:?}", team);
let empty: [i32; 0] = [];
println!("from empty: {:?}", empty.choose(&mut rng));
}

Output:

shuffled: ["K", "J", "10", "Q", "A"]
picked: Some("red")
team: ["K", "J"]
from empty: None
▶ Try it Yourself

choose returns an Option, because an empty slice has nothing to choose. These methods work on arrays, vectors and slices alike. Replace the seeded rng with rand::rng() and you get a new shuffle every run.

Let’s write a function that rolls dice. The trick is the parameter type: &mut impl Rng means “any random generator”. In the real game you pass rand::rng(). In a test you pass a seeded StdRng and know exactly what comes out:

use rand::rngs::StdRng;
use rand::{Rng, RngExt, SeedableRng};
/// Roll `count` dice with `sides` sides each.
fn roll(rng: &mut impl Rng, count: u32, sides: u32) -> Vec<u32> {
(0..count).map(|_| rng.random_range(1..=sides)).collect()
}
fn main() {
let mut rng = StdRng::seed_from_u64(1);
for (count, sides) in [(2, 6), (1, 20), (3, 4)] {
let rolls = roll(&mut rng, count, sides);
let total: u32 = rolls.iter().sum();
println!("{count}d{sides}: {:?} = {total}", rolls);
}
}

Output:

2d6: [5, 6] = 11
1d20: [16] = 16
3d4: [3, 1, 2] = 6
▶ Try it Yourself

2d6 is board-game shorthand for “two six-sided dice”. Passing the generator in, instead of calling rand::rng() inside roll, is a good habit: it keeps the function easy to test.

Here we pick 16 characters from an allowed set. The set leaves out look-alike characters like 0/O and 1/l, so the password is easier to type:

use rand::seq::IndexedRandom;
use rand::Rng;
const CHARSET: &[u8] = b"ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%&*";
fn password(rng: &mut impl Rng, length: usize) -> String {
(0..length)
.map(|_| *CHARSET.choose(rng).unwrap() as char)
.collect()
}
fn main() {
let mut rng = rand::rng();
let pw = password(&mut rng, 16);
println!("length: {}", pw.len());
println!("only allowed characters: {}", pw.bytes().all(|b| CHARSET.contains(&b)));
// println!("{pw}"); // uncomment to see it: different on every run
}

Output:

length: 16
only allowed characters: true
▶ Try it Yourself

b"..." is a byte string: a &[u8] of ASCII characters. choose picks one byte, and as char turns it into a character. Notice we used rand::rng() here, never a seeded generator. A password from a fixed seed is the same password every time, which is no password at all.

The Guessing Game project picks its secret number with a clock trick, so it can stay standard-library only. With rand, the whole secret_number function becomes one line:

let secret: u32 = rand::random_range(1..=100);

Everything else in the game stays the same. Here’s a version you can run in the Playground. The secret comes from a seeded generator, and the computer guesses by always picking the middle of the remaining range:

use rand::rngs::StdRng;
use rand::{RngExt, SeedableRng};
use std::cmp::Ordering;
fn main() {
// In the real game: let secret: u32 = rand::random_range(1..=100);
let mut rng = StdRng::seed_from_u64(99);
let secret: u32 = rng.random_range(1..=100);
let (mut low, mut high) = (1, 100);
let mut attempts = 0;
loop {
let guess = (low + high) / 2;
attempts += 1;
match guess.cmp(&secret) {
Ordering::Less => { println!("{guess} is too small"); low = guess + 1; }
Ordering::Greater => { println!("{guess} is too big"); high = guess - 1; }
Ordering::Equal => { println!("{guess} is right! Found in {attempts} attempts."); break; }
}
}
}

Output:

50 is too small
75 is too small
88 is too small
94 is too small
97 is too small
99 is right! Found in 6 attempts.
▶ Try it Yourself

Fun fact: this “halve the range” strategy never needs more than 7 guesses for 1 to 100.

Short answer: rand::rng() is designed to be hard to predict, but for serious secrets, use tools made for the job.

  • rand::rng() uses a cryptographically secure algorithm (currently ChaCha12). It is seeded from your operating system’s random source, through the getrandom crate, and reseeds itself regularly. The rand docs call it a “fast, reasonably secure generator”, and say they can’t promise it fits every security need.
  • For encryption keys, API tokens, and other high-stakes secrets, get the bytes straight from the operating system with getrandom (or rand::rngs::SysRng), or better, let a dedicated crypto library generate the key for you.
  • Never use a seeded generator (seed_from_u64) or the fast SmallRng for anything secret. Anyone who knows or guesses the seed can produce the same “random” values.

For games, simulations, shuffling and test data, rand::rng() is exactly right.

Importing Rng instead of RngExt. In rand 0.9 the methods lived on Rng, so older code does use rand::Rng;. With rand 0.10 that gives:

use rand::Rng;
fn main() {
let mut rng = rand::rng();
let n = rng.random_range(1..=6);
println!("{n}");
}
error[E0599]: no method named `random_range` found for struct `ThreadRng` in the current scope
--> src/main.rs:5:17
|
5 | let n = rng.random_range(1..=6);
| ^^^^^^^^^^^^
|
= help: items from traits can only be used if the trait is in scope
help: trait `RngExt` which provides `random_range` is implemented but not in scope; perhaps you want to import it
|
1 + use rand::RngExt;
|

The compiler tells you the fix: use rand::RngExt;.

Copying code written for rand 0.8. Very old examples use thread_rng() and gen_range():

fn main() {
let n = rand::thread_rng().gen_range(1..=6);
println!("{n}");
}
error[E0425]: cannot find function `thread_rng` in crate `rand`
--> src/main.rs:2:19
|
2 | let n = rand::thread_rng().gen_range(1..=6);
| ^^^^^^^^^^ not found in `rand`

The new names are rand::rng() and random_range(). Likewise, gen() became random() and gen_bool() became random_bool().

Off-by-one ranges. random_range(1..6) never returns 6. For a die, write 1..=6.

  • Add the crate with cargo add rand. Rust’s standard library has no random number generator of its own.
  • For quick values use rand::random_range(1..=6) and rand::random::<T>().
  • For many values, use let mut rng = rand::rng(); with use rand::RngExt; for methods like random_range and random_bool.
  • SliceRandom::shuffle mixes a slice; IndexedRandom::choose picks one item and returns an Option.
  • StdRng::seed_from_u64(n) gives the same sequence every run: great for tests and repeatable game levels, never for secrets.
  • For keys and tokens, use the OS random source (getrandom) or a dedicated crypto crate.

Quick check

1. Which call can return 6?

2. With rand 0.10, rng.random_range(..) fails with "no method named random_range". What is missing?

3. Why would you use StdRng::seed_from_u64(42)?

4. What does choose return when you call it on an empty slice?

Score: 0 / 4