NOETRION

Choose a topic

TUTORIAL · 7 MIN READ

NumPy Random Generators: Reproduce a Workflow, Not Just a Seed

Separate random state by purpose, create reproducible task streams, and document the limits of seed-based reproducibility.

Reviewed October 9, 2026. Examples target NumPy 2.3.5 and use synthetic draws.

Two notebooks set the same seed, yet their final simulated samples differ. One notebook added a random diagnostic plot before running the simulation. That extra draw advanced the shared generator, so the later operation consumed a different part of the stream. The seed was the same; the sequence of operations was not.

NumPy's Generator makes random state an explicit object that can be passed to a function. Reproducibility becomes easier to reason about when that object has a clear owner and purpose. This tutorial explains state consumption, independent task streams, and the environment information needed to interpret a repeatable result honestly.

1. Understand the state behind the seed

A pseudorandom generator evolves an internal state and produces values through deterministic algorithms. A seed initializes that process; it is not a label that automatically resets every subsequent call. After a generator produces numbers, later calls continue from its updated state. Recreating a generator with the same setup starts again; reusing the object advances it.

The BitGenerator supplies the underlying random bits, while Generator exposes methods for distributions and sampling. NumPy's default_rng is a convenient constructor. When an exact algorithm choice matters, construct the desired bit generator explicitly and record it, rather than assuming the default will remain unchanged across all future software versions.

For application functions, accept a generator argument instead of changing global random state. The caller can then decide whether two operations should share a stream or receive separate ones. This makes dependencies visible and prevents an unrelated helper from silently changing the random values used by a later experiment.

Separate generator streams isolate simulation from diagnostic draws
Original schematic: a root seed derives separate simulation and diagnostic streams; consuming diagnostic draws does not advance the simulation stream. Open full-size diagram.

2. Test repeatability with the same call sequence

import numpy as np

first = np.random.default_rng(20261009)
second = np.random.default_rng(20261009)
a = first.integers(0, 1000, size=8)
b = second.integers(0, 1000, size=8)
assert np.array_equal(a, b)
following = first.integers(0, 1000, size=8)
reference = np.random.default_rng(20261009)
reference.integers(0, 1000, size=8)
expected = reference.integers(0, 1000, size=8)
assert np.array_equal(following, expected)
print(a, following)

This program checks replay within the installed environment instead of publishing a sequence of numbers as a promise for every platform. The second block of draws is reproducible because the reference follows the same calls and arguments. Adding or changing a call changes the workflow being replayed, even if the initial seed stays fixed.

Do not infer stream equivalence from a distribution alone. Changing the requested shape, switching distribution methods, or replacing a vectorized call with repeated scalar calls can alter the exact sequence. NumPy's compatibility policy includes strict conditions, including the call sequence and environment. Equal statistical intent is a weaker requirement than equal generated values.

Reproducibility layer What to record What it can establish
Initialization Seed and bit generator Starting random state policy
Execution Method calls, arguments, order Which stream segment was consumed
Environment NumPy build, versions, platform Context for exact replay
Task mapping Stable task identifiers Which stream belongs to which task
Evidence Saved inputs and outputs Recoverable results beyond a seed

3. Split streams by purpose

A simulation and a diagnostic visualization usually do not need to consume the same stream. Deriving separate child streams gives each operation its own state. The split should be part of the experiment configuration, with a stable association between child stream and purpose. Naming that association is more useful than distributing anonymous generators in arbitrary order.

import numpy as np

def make_streams():
    children = np.random.SeedSequence(20261009).spawn(2)
    return [np.random.default_rng(child) for child in children]

simulation, diagnostics = make_streams()
expected_simulation, expected_diagnostics = make_streams()
diagnostics.normal(size=1000)
observed = simulation.normal(size=6)
expected = expected_simulation.normal(size=6)
assert np.array_equal(observed, expected)
expected_diagnostics.normal(size=1000)
assert np.array_equal(diagnostics.normal(size=6),
                      expected_diagnostics.normal(size=6))
print(observed)

The assertion verifies isolation of state consumption. It does not mathematically prove independence from six displayed values. NumPy's stream construction is designed to provide streams that are independent with very high probability. A short empirical comparison cannot certify that property, and two different streams can legitimately produce an equal individual value.

Repeatedly calling spawn on the same SeedSequence advances its child allocation. Recreating the root and repeating the allocation reconstructs the mapping used here. Saving only the root entropy while forgetting how children were assigned is insufficient when the allocation process has changed. Treat the stream tree as part of the workflow configuration.

4. Bind parallel streams to stable tasks

Parallel scheduling is often nondeterministic. If whichever worker finishes first receives the next stream, the same seed may attach different draws to different logical tasks on the next run. Assign streams by stable task identity before scheduling, or derive them from documented identifiers. Worker number and task identity are not necessarily the same thing.

NumPy documents combining an integer task or worker identifier with root seed material as a sequence of integers. Prefer a clear, deterministic ID scheme over adding a small worker number to a root seed. The latter can produce overlapping seed assignments across nearby repeated experiments. Do not use Python's process-randomized built-in string hash as an undocumented stable task ID.

The design choice depends on the desired invariance. A fixed spawned list works when task order is stable. Identifier-based derivation can preserve existing task streams when unrelated tasks are inserted. In either case, record the mapping and avoid reusing the same stream identity for tasks that were intended to have separate random inputs.

5. Separate exact replay from robust conclusions

One reproducible run can still be unrepresentative. A fixed seed is valuable for debugging and comparisons, but conclusions about a stochastic procedure may require repeated seeds and a distribution of outcomes. Report variability instead of selecting the one seed that produces a pleasing result. Exact replay and scientific robustness answer different questions.

When comparing two procedures, shared random inputs can sometimes reduce comparison noise, but that coupling should be intentional. Separate streams can also be appropriate when the procedures need independent trials. The generator API does not choose the experimental design. Document why the streams are shared or separated and how repetitions are paired.

Do not call a generator cryptographically secure merely because its output looks irregular. Simulation generators are for numerical randomness under their documented purposes. Passwords, tokens, and security secrets require an appropriate security-oriented randomness source. This tutorial's seeds are public and fixed specifically to support explanation and replay, not secrecy.

6. Capture enough evidence for long-term reproduction

Record library versions, bit-generator class, seed material, task mapping, inputs, code revision, and output artifacts. If exact arrays are central to a published result, saving those arrays can be more durable evidence than expecting a seed to regenerate them forever. A package lock helps identify dependencies but does not describe every platform-level numerical difference.

For resumable computation, saving bit-generator state can support continuation within a compatible setup. Store it carefully with the algorithm and environment metadata. A state snapshot from the middle of a run is different from the initial seed: it describes where execution had reached. Restoring it while changing the subsequent calls still changes the generated workflow.

Useful checks verify that identical setups replay, extra draws in one purpose do not affect another, and stable tasks retain their assigned streams when scheduling order changes. Avoid tests that demand every pair of sampled values differ; random values can coincide. Test the construction and mapping rules rather than a simplistic visual notion of randomness.

7. Interpret these examples within their limits

The fixtures demonstrate deterministic replay and state isolation on small synthetic draws. They do not benchmark speed, establish the quality of a real simulation model, or guarantee byte-for-byte equality across every NumPy release and machine. The model assumptions, input data, and numerical operations remain independent sources of error.

A practical workflow makes randomness visible: generators are owned, purposes are named, tasks have stable identities, and results retain their configuration. That documentation turns a seed from a reassuring number in a notebook into one component of an auditable experiment. It also makes legitimate changes easier to explain when a revised workflow produces different samples.

Sources and further reading