Skip to course content
Free Python statistics course

Statistical Data Analytics with Python

Unit 01.02: Seed a generator, not the process

np.random.seed mutates a hidden global. np.random.default_rng hands you an object whose state travels with it.

Seed a generator, not the process

Any bootstrap, permutation test, simulation or random assignment draws random numbers, and without a seed each run gives a different answer — usually similar, occasionally different enough to change what you report.

NumPy has two APIs. The legacy one, np.random.seed(), sets a single process-wide state that any library call can consume from, so an unrelated function drawing one number shifts everything after it. The Generator API, np.random.default_rng(seed), gives you an object you pass explicitly, and nothing else can touch it.

Seed once, at the top, and pass the generator down. Never choose a seed after seeing what it does to the answer.

This block shows a reproducible pair of draws, then the legacy API being disturbed.

import numpy as np

# The modern API: an explicit generator you pass around.
rng = np.random.default_rng(2026)
first = rng.normal(100, 15, 50).mean()

rng = np.random.default_rng(2026)
second = rng.normal(100, 15, 50).mean()

third = rng.normal(100, 15, 50).mean()      # same generator, next draw

print(f"Seeded run 1 : {first:.4f}")
print(f"Seeded run 2 : {second:.4f}   identical: {first == second}")
print(f"Next draw    : {third:.4f}   identical: {first == third}")
print(f"Difference   : {abs(first - third):.4f}"
      f"  ({abs(first - third) / first * 100:.2f}% of the estimate)")
print()

# The legacy API mutates hidden global state, so any library call can
# consume draws and shift everything downstream.
np.random.seed(7)
before = np.random.normal(size=3)
np.random.seed(7)
np.random.normal(size=1)                    # a library function draws once
after = np.random.normal(size=3)
print("Legacy global seeding:")
print("  same seed, no interference:", np.round(before, 4))
print("  after one hidden draw    :", np.round(after, 4))
print("Prefer default_rng: the state travels with the object, not globally.")

Two draws from freshly seeded generators both give 103.2562 and compare as identical. The next draw from the same generator gives 98.0936 — a difference of 5.1626, or 5.00% of the estimate, easily enough to move an interval's endpoints. The legacy demonstration is the important one: after the same seed, a single hidden draw shifts the sequence, so the 'reproducible' values now start one position later.

The mistake this prevents

The mistake is np.random.seed() at the top of a notebook and assuming the run is fixed. Any imported function that draws a random number silently changes every subsequent result.

Takeaway

Use rng = np.random.default_rng(seed) once and pass rng explicitly. Record the seed in the report, and treat a result that changes between seeded runs as a bug.