Skip to content
Select theme

How to generate a random number in a range

Short answer: the rand crate. rand::rng() gives you a thread-local generator, and random_range(1..=6) returns a value from that range with equal probability for every number in it. The trap is the range syntax itself: 1..6 excludes the 6, so a die rolled with random_range(1..6) can never land on it. Write ..= whenever the top of the range is meant to be a possible result.

From a fresh project — cargo new dice-demo, add the dependency to Cargo.toml, then replace src/main.rs with each block below.

Cargo.toml
[dependencies]
rand = "0.10.3"
src/main.rs
use rand::RngExt;
fn main() {
let mut rng = rand::rng();
let roll = rng.random_range(1..=6);
println!("You rolled a {roll}");
}
You rolled a 4

Run it a few times and the number changes — that is the point. random_range is uniform, so every value in the range shows up equally often over many rolls. It works on all the integer types (and floats, below), and for a one-off value there is a free function that skips the explicit generator: rand::random_range(1..=6).

random_range takes an ordinary Rust range, and the two spellings differ in exactly one value. A million samples make the difference visible:

src/main.rs
use rand::RngExt;
fn main() {
let mut rng = rand::rng();
let (mut lo, mut hi) = (u32::MAX, 0);
for _ in 0..1_000_000 {
let v: u32 = rng.random_range(0..10);
lo = lo.min(v);
hi = hi.max(v);
}
println!("0..10 gave min={lo} max={hi}");
let (mut lo, mut hi) = (u32::MAX, 0);
for _ in 0..1_000_000 {
let v: u32 = rng.random_range(0..=10);
lo = lo.min(v);
hi = hi.max(v);
}
println!("0..=10 gave min={lo} max={hi}");
}
0..10 gave min=0 max=9
0..=10 gave min=0 max=10

A million rolls of 0..10 never produced a 10, because a 10 is not in that range: 0..10 means 0 through 9, and 0..=10 includes the 10. This is the same range syntax a for loop uses — for i in 0..10 also stops at 9 — which is easy to forget when the requirement reads “a number between 1 and 100”. The answer is random_range(1..=100); random_range(1..100) tops out at 99.

The names in older tutorials do not compile

Section titled “The names in older tutorials do not compile”

Search results are still full of rand 0.8 code: thread_rng().gen_range(...). Against current rand that fails before it runs:

src/main.rs
use rand::Rng;
fn main() {
let mut rng = rand::rng();
let roll = rng.gen_range(1..=6);
println!("{roll}");
}
Terminal window
cargo run -q
warning: unused import: `rand::Rng`
--> src/main.rs:1:5
|
1 | use rand::Rng;
| ^^^^^^^^^
|
= note: `#[warn(unused_imports)]` (part of `#[warn(unused)]`) on by default
error[E0599]: no method named `gen_range` found for struct `ThreadRng` in the current scope
--> src/main.rs:5:20
|
5 | let roll = rng.gen_range(1..=6);
| ^^^^^^^^^ method not found in `ThreadRng`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `dice-demo` (bin "dice-demo") due to 1 previous error; 1 warning emitted

The old names are gone, not deprecated: rand renamed them because gen became a reserved keyword in edition 2024, so gen_range turned into random_range, thread_rng() into rng(), and the convenience methods moved onto the RngExt trait — which is why the import at the top is use rand::RngExt;. Translating an older tutorial is a three-part rename: rand::thread_rng() → rand::rng(), .gen_range(r) → .random_range(r), and use rand::Rng → use rand::RngExt.

When the same numbers are the point: seed it

Section titled “When the same numbers are the point: seed it”

Random output makes tests and examples unreliable. A seeded generator produces the same sequence from the same seed, every run:

src/main.rs
use rand::{rngs::StdRng, RngExt, SeedableRng};
fn main() {
let mut rng = StdRng::seed_from_u64(42);
let a = rng.random_range(1..=100);
let b = rng.random_range(1..=100);
println!("{a} {b}");
}
14 53

Anyone who runs this gets 14 and 53, on any machine, forever — which is exactly what a failing test needs. The trade-off is secrecy: a seeded StdRng is predictable by design, so keep rand::rng() (which is built to be unpredictable) for anything an attacker should not be able to guess, and reach for the seed only when reproduction is the goal.

src/main.rs
use rand::RngExt;
fn main() {
let mut rng = rand::rng();
let x = rng.random_range(0.0..1.0);
println!("{x:.3}");
}
0.728

The output varies here too. A half-open range is the natural spelling for floats — 0.0..1.0 covers everything from 0.0 up to (but not reaching) 1.0, the usual convention for a random fraction — and ..= is legal if a 1.0 result should be possible.