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 callModern equivalentDifference
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.

Performance: einsum, ufunc tricks and avoiding copies File I/O: save, load, npz and memory-mapped arrays

Last refreshed 2026-09-18.