Random number generation with the Generator API
Use default_rng correctly, pick the right distribution, and make results reproducible without poisoning global state.
Generator versus the legacy functions
np.random.default_rng() returns an explicit Generator object. It has better statistical properties than the legacy np.random.* functions and, crucially, it does not touch global state — so two libraries cannot interfere with each other's streams.
import numpy as np
rng = np.random.default_rng(42) # a seeded, independent stream
rng.random(3) # floats in [0, 1)
rng.integers(0, 10, size=5) # ints; high is exclusive
rng.normal(loc=0.0, scale=1.0, size=(2, 3))
rng.uniform(-1, 1, 4)
rng.binomial(10, 0.5, size=4)
rng.choice(["a", "b", "c"], size=2, p=[0.5, 0.25, 0.25])
rng.choice(5, size=3, replace=False) # sample without replacement
rng.shuffle(arr) # in place
perm = rng.permutation(arr) # returns a copy
# legacy: still works, but do not start new code here
np.random.seed(0)
np.random.rand(3)| Legacy call | Modern equivalent | Difference |
|---|---|---|
np.random.seed(n) | np.random.default_rng(n) | Global versus explicit state |
np.random.rand(3) | rng.random(3) | Same range, better algorithm |
np.random.randn(3) | rng.standard_normal(3) | normal also takes loc/scale |
np.random.randint(0, 9, 3) | rng.integers(0, 9, 3) | Same exclusive upper bound |
np.random.choice(5, 3) | rng.choice(5, 3) | Identical signature |
np.random.shuffle(a) | rng.shuffle(a) | Identical behaviour |
Seeds, reproducibility and parallelism
A seed fixes one stream. Reproducibility means recreating the same stream at the same point, so a Generator must be created once and passed into the function that consumes it — not called freshly inside every helper.
def simulate(seed, n=5):
rng = np.random.default_rng(seed)
return rng.normal(size=n)
simulate(0) # identical every time
simulate(0)
def bootstrap(data, rng): # take the generator as a parameter
idx = rng.integers(0, len(data), size=len(data))
return data[idx].mean()
rng = np.random.default_rng(7)
bootstrap(np.arange(100.0), rng)
# parallel work: independent child streams, no overlap
children = rng.spawn(4)
[child.random(2) for child in children]Choosing a distribution
rng.normal(0, 1, 1000) # heights, measurement error
rng.poisson(3.0, 1000) # counts of rare events per interval
rng.exponential(2.0, 1000) # waiting times between events
rng.beta(2, 5, 1000) # proportions and rates in [0, 1]
rng.lognormal(0, 0.5, 1000) # multiplicative effects, incomes
rng.multinomial(10, [0.2, 0.8], size=3) # dice-like draws
rng.random(3) < 0.3 # Bernoulli with p = 0.3
rng.integers(1, 7, size=10) # a fair die, values 1 to 6⚠️
Never call
np.random.seed() inside a library function. It resets the global stream for every other caller in the process, which turns reproducible tests into flaky ones. Accept a Generator argument instead.FAQ
Is default_rng(42) the same as np.random.seed(42)?
No. The two use different algorithms and produce completely different numbers. Migrating means updating expected values in your tests, not just renaming calls.
How do I get the same split across a distributed job?
Create one parent generator with a fixed seed and derive independent children with
rng.spawn(n), giving each worker its own child. Do not reuse one generator from several processes.Related
Performance: einsum, ufunc tricks and avoiding copies File I/O: save, load, npz and memory-mapped arrays
Last refreshed 2026-09-18.