Use NumPy Generator instead of global random state for reproducible stochastic code
Replace np.random.seed() with an explicit numpy.random.Generator to isolate random streams, improve reproducibility, and avoid silent coupling between libraries.
08 Jun 2026, 00:43 UTC

The problem with global random state
Calling np.random.seed() mutates NumPy's global random state. Every subsequent call to np.random.random() or np.random.normal() draws from that same stream, including calls inside libraries you imported after seeding. The result is silent coupling: a change in import order or a new dependency can alter simulation outputs, and tests become order-dependent because one test's random draws consume state meant for another. Parallel workers that inherit the global state can also produce duplicate or correlated draws.
The useful takeaway is to stop relying on the global API. Create a numpy.random.Generator with numpy.random.default_rng(), seed it explicitly, and pass the instance to every function that needs randomness. Generator is backed by the PCG64 bit generator, a pseudorandom number generator with better statistical properties than the legacy Mersenne Twister used by RandomState.
Requirements
- NumPy 1.17 or later.
- Non-cryptographic randomness only. Generator is not suitable for tokens, keys, or security boundaries.
- A need to reproduce runs, isolate streams across tasks, or avoid global-state side effects.
Smallest suitable design
Instantiate one Generator per logical task and pass it explicitly:
import numpy as np
from numpy.random import default_rng, SeedSequence
def run_trials(rng, n_trials):
return rng.normal(loc=0.0, scale=1.0, size=n_trials)
seed = 20261111
rng = default_rng(seed)
results = run_trials(rng, 1000)
default_rng(seed) returns a Generator whose state advances with every draw. Because rng is an ordinary argument, the function does not touch NumPy's global state, and callers can supply a different stream for testing or production.
For multiple workers or processes, spawn independent child streams from a SeedSequence rather than reusing the same seed:
ss = SeedSequence(seed)
child_seeds = ss.spawn(4)
worker_rngs = [default_rng(child) for child in child_seeds]
Each child seed produces a statistically independent stream, which is the appropriate pattern for distributing stochastic work across processes.
Trust and data boundaries
- Mutable and not thread-safe. A Generator object changes state as it is used. Do not share one instance across threads without external locking; create one Generator per thread instead.
- Not cryptographically secure. PCG64 is designed for simulation and modeling, not secrets. Use Python's
secretsmodule oros.urandom()for tokens, keys, or any security boundary. - State format may change. The dictionary returned by
rng.bit_generator.stateis tied to a NumPy version. Do not archive raw state for long-term reproducibility unless you also pin the NumPy version.
Operational checks
Run this in a terminal or Python REPL. No special permissions are required.
python -c 'import numpy as np; rng = np.random.default_rng(123); print(rng.random(3)); print(rng.random(3))'
Expected check: the two printed arrays should differ, confirming that the stream advanced between calls.
Confirm the backing algorithm and inspect state:
import numpy as np
rng = np.random.default_rng(123)
print(type(rng.bit_generator).__name__) # expect PCG64
print(rng.bit_generator.state)
Risk: do not copy the state dictionary across NumPy upgrades expecting identical streams. Store the seed and NumPy version instead, or regenerate from a pinned environment.
Failure modes and conditions that change the design
| Condition | Implication | Adjusted design |
|---|---|---|
| Multi-threaded application | Generator is mutable and not thread-safe | Create one Generator per thread, or guard shared access with a lock |
| Security tokens or keys | PCG64 is not cryptographically secure | Use secrets.token_bytes() or os.urandom() |
| Long-term archival of results | BitGenerator state format may change between NumPy versions | Pin NumPy version and store the seed, not the raw state dictionary |
| Legacy library requires RandomState | Some older code expects numpy.random.RandomState | Use Generator internally; convert at the boundary only if unavoidable |
Limitations
Generator does not solve every randomness problem. It is still a pseudorandom generator, so streams are deterministic given the same seed and NumPy version. It is not thread-safe by default, and it must not be used where an attacker could benefit from predicting output. If any of those constraints apply, choose a different tool or add explicit isolation.
Practical verification
- Run the two-call snippet above and confirm the outputs differ.
- Check
type(rng.bit_generator).__name__ == 'PCG64'. - Spawn child seeds in a subprocess and confirm the child stream produces different values than the parent.
If these checks pass, the Generator is behaving as an independent, advancing stream rather than relying on global state.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.