Simulations, train/test splits, bootstrapping and test data all need randomness you can reproduce.
The modern way to get random numbers is a Generator object:
rng = np.random.default_rng(seed=42)All draws are methods on it: rng.random(size) for floats in [0, 1), rng.integers(low, high, size) (high is exclusive unless endpoint=True), rng.normal(loc, scale, size), rng.uniform, rng.poisson, rng.choice(a, size, replace=False, p=weights), rng.permutation(x) (returns a shuffled copy) and rng.shuffle(x) (in place, along the first axis).
Pass a seed to make results reproducible: the same seed and the same sequence of calls always give the same numbers. Without a seed, fresh entropy from the operating system is used. Create one Generator at the start of a program or function and pass it around, instead of re-seeding all over the place. A common pattern is accepting an rng parameter that defaults to None and calling np.random.default_rng(rng), which accepts a seed, an existing Generator or None.
The default bit generator is PCG64, which is fast and statistically strong. For parallel work, derive independent streams with rng.spawn(n) (or np.random.SeedSequence(seed).spawn(n)) rather than seeding workers with seed + i, which can produce correlated streams.
You will still see the legacy API in older code: np.random.seed(0) and np.random.rand(...), np.random.randint(...). These use one hidden global state, so any library calling them changes your sequence. They are kept for compatibility only; new code should use a Generator.
None of this is suitable for passwords or tokens: use Python's secrets module for cryptographic randomness.
Example
import numpy as np
rng = np.random.default_rng(seed=42)
print(rng.integers(1, 7, size=5)) # five dice rolls, 1..6
print(rng.normal(loc=0, scale=1, size=3))
print(rng.choice(["a", "b", "c"], size=2, replace=False))
data = np.arange(10)
idx = rng.permutation(len(data)) # random order of indices
train, test = data[idx[:8]], data[idx[8:]]
print(len(train), len(test)) # 8 2
# Same seed, same numbers
r1 = np.random.default_rng(7).random(3)
r2 = np.random.default_rng(7).random(3)
print(np.array_equal(r1, r2)) # True
# Independent streams for parallel workers
workers = np.random.default_rng(123).spawn(4)
print([int(w.integers(100)) for w in workers])Remember
- Create a Generator with
np.random.default_rng(seed)and call methods on it - A seed makes results reproducible; pass the rng around rather than re-seeding
integersexcludeshighby default;choice(replace=False)samples without repeats- Use
spawnfor parallel streams; avoid the legacy globalnp.random.seed
Watch out for
- Calling
np.random.default_rng(42)inside a loop, which restarts the same sequence on every iteration.