Random numbers with Generator

NumPy · Practical · 10 min

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
  • integers excludes high by default; choice(replace=False) samples without repeats
  • Use spawn for parallel streams; avoid the legacy global np.random.seed

Watch out for

  • Calling np.random.default_rng(42) inside a loop, which restarts the same sequence on every iteration.

Check yourself

  1. Why is the Generator API preferred over `np.random.seed(0); np.random.rand(3)`?
  2. What range can `rng.integers(0, 10)` return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: numpy.org/doc/stable/reference/random/generator.html