Articles
NumPy Randomness Interview Questions: Generators, Seeds, and Parallel Reproducibility
Practice NumPy randomness interview questions. Trace Generator state, seed parallel tasks, distinguish retries from replications, and define replay limits.
NumPy randomness interview questions are not solved by saying “set a seed.” A convincing answer identifies who owns the random state, what advances it, and how each parallel task receives a reproducible stream. The seed is an input to that design, not a complete replay contract.
For data science and numerical Python interviews, focus on three decisions: use an explicit Generator, avoid recreating it inside independent trials, and attach streams to stable task identities when scheduling can change. Then state the environment and call sequence under which you expect identical output.
This article covers simulation and testing, not cryptographic randomness.
NumPy randomness interview questions: what owns the state?
A pseudorandom number generator produces a deterministic sequence from internal state. A seed initializes that state; drawing values advances it. Reproducibility means recreating the relevant initialization and operations, not repeatedly resetting the state whenever a result looks inconvenient.
NumPy separates the distribution-facing Generator from its underlying BitGenerator. The former offers operations such as normal draws and sampling; the latter supplies the bit stream. This distinction is documented in the official Generator reference and BitGenerator overview.
Start with an explicit dependency:
import numpy as np
def simulate_returns(rng, n):
return rng.normal(loc=0.0, scale=1.0, size=n)
rng = np.random.default_rng(17)
first = simulate_returns(rng, 3)
second = simulate_returns(rng, 3)
first and second consume different portions of one stream. Constructing another generator with seed 17 reproduces the beginning of that stream in the same tested environment. Passing the existing generator into another function shares its advancing state; it does not create an independent experiment.
An interviewer might ask whether default_rng(existing_generator) clones it. It returns the supplied generator, so later draws still affect that object. If you need another stream, make that intention explicit rather than treating a constructor-looking call as evidence of new ownership. The useful explanation is about identity and state, not syntax alone.
Does np.random.seed reset every generator?
No. The legacy module-level random API and a separately constructed Generator are different state owners. A reset of the legacy singleton does not reset a generator stored in your function or object. This follows the API distinction in NumPy’s random generation documentation.
Our original fixture, run with NumPy 2.5.3, draws three values from default_rng(17), calls np.random.seed(17), and then draws three more. The second draw matches a control generator that simply advanced through the same first three values. The legacy reset did not rewind it.
| Operation | State affected | Interview implication |
|---|---|---|
np.random.seed(17) | Legacy module-level state | Does not control every RNG object |
default_rng(17) | Newly initialized generator | Repeats its initial sequence when recreated |
rng.normal(size=3) | Existing generator | Advances the owned stream |
Passing rng to a helper | Same generator object | Call order becomes part of replay |
This matters in a mixed codebase. A library may accept an RNG object while an older utility calls np.random.random. A single global seed cannot be assumed to govern both. Identify those paths before promising deterministic tests.
The follow-up is usually dependency design: accept rng explicitly, document whether the function consumes it, and avoid hidden draws in unrelated logging or diagnostics. Otherwise adding a debug sample changes every later result, even though the root seed never changed.
Why does reseeding inside a loop duplicate trials?
Consider a Monte Carlo estimate implemented like this:
bad = [np.random.default_rng(17).normal(size=1000)
for _ in range(20)]
Every iteration starts from the same state and requests the same operation. These are twenty copies of one simulated sample, not twenty independently generated replications. Averaging their estimates does not buy the uncertainty reduction expected from twenty independent runs.
For a sequential simulation, initialize once:
rng = np.random.default_rng(17)
trials = [rng.normal(size=1000) for _ in range(20)]
Now the calls advance the stream. This is appropriate when replay depends on the same ordered sequence of trials. It is not automatically stable if you insert a trial, change earlier sample sizes, or distribute the work differently.
Distinguish two testing goals. A deterministic unit test wants a repeatable fixture so failures are inspectable. A simulation study wants varied replications generated under a defensible stream design. Fixed seeds can support both, but duplicated samples cannot stand in for independent replications.
A particularly useful interview counterexample is a confidence interval that gets narrower when you increase a loop count despite every loop drawing the same sample. Ask how uncertainty was computed. If it treats duplicated rows as new information, the statistical problem is larger than the seeding bug.
Parallel reproducibility: seed the task, not its accidental worker
Suppose tasks 11, 12, and 13 can run on any available worker. Seeding worker 0 and worker 1 gives deterministic worker streams, but task 12 may consume a different stream after a scheduling change. The same global seed is compatible with different task-level results.
Use a stable identifier for the unit that must replay:
def run_task(task_id, root_seed=20261004):
rng = np.random.default_rng([task_id, root_seed])
return rng.normal(size=4)
forward = {k: run_task(k) for k in [11, 12, 13]}
reordered = {k: run_task(k) for k in [13, 11, 12]}
assert all(np.array_equal(forward[k], reordered[k])
for k in forward)
The list seed combines a deterministic integer task ID with the root seed. NumPy documents this strategy, including placing varying IDs before a shared root seed, in its parallel randomness guidance. Our fixture verifies task-wise equality under the two listed schedules; it does not prove statistical independence from a few observed values.
A worker identity is appropriate only when the worker itself is the replay unit. A stable task identity survives reassignment between workers.
Do not derive the identifier from Python’s process-dependent string hash(). Use a recorded integer ID or a deliberately specified stable encoding. Uniqueness matters: two logically different tasks given the same ID and root seed request the same stream.
Retries need an explicit policy too. Reusing the task ID reproduces the original random decisions, which helps fault recovery. It does not create a fresh independent trial. If the task is intentionally a new replication, assign a distinct replication ID. Keep retry count and scientific replication count separate.
Even deterministic task streams do not guarantee deterministic aggregate output. Floating-point reduction order can change when task completion order changes. Sort results by task ID or use a specified reduction scheme when exact replay of the final statistic matters.
When should you use SeedSequence.spawn?
Spawning is useful when work naturally forms a hierarchy: an experiment creates replications, and each replication creates substreams. The official parallel generation reference explains how SeedSequence mixes the spawning path into initialization.
root = np.random.SeedSequence(20261004)
children = root.spawn(3)
streams = [np.random.default_rng(child) for child in children]
Assign these children to a recorded task ordering. Reconstructing a fresh root and repeating the same spawning structure can replay the fixture. Calling spawn again on an already advanced spawning tree allocates more children; it is not a request to restart the previous allocation.
The distinction from task-ID seeding is operational. Spawning makes a structured tree easy to express. Stable-ID seeding makes arbitrary task lookup easy without recreating all earlier allocations. Neither removes the need to record which stream belongs to which work unit.
Avoid claiming the resulting streams are mathematically guaranteed independent in every possible use. NumPy describes high-probability separation, not a proof obtained by comparing a handful of sample values. A test that two streams differ catches a duplicated fixture; it cannot certify the statistical quality of an entire simulation.
Also avoid copying a live generator into child processes and assuming each copy will evolve differently. If workers receive identical state and perform identical operations, they can produce duplicate sequences. Construct child streams intentionally and pass their initialization information across the process boundary.
What can the same seed fail to reproduce?
NumPy’s compatibility policy defines a narrow stream-compatibility expectation involving the bit generator, seed, method sequence, arguments, build, environment, and machine. A seed alone does not guarantee unchanged distribution outputs across arbitrary upgrades.
For an interview replay record, capture:
- Root seed and task or spawning-path identifiers.
- NumPy version and chosen bit-generator type.
- Distribution calls, argument values, requested shapes, and ordering.
- Input data version and relevant numerical environment.
- Aggregation order and any other libraries’ RNG configuration.
Changing a requested array size is a changed argument. Do not promise that five scalar calls are always interchangeable with one five-element call across all supported algorithms and versions. Pin the actual workload you need to reproduce.
For a mid-run checkpoint, save the bit-generator state using an appropriate trusted serialization format and retain the matching implementation/environment. A seed restarts initialization; state restores a position. That distinction prevents a recovery system from replaying already completed draws accidentally.
Finally, this random API is not a security credential generator. If the prompt moves to password-reset tokens or unpredictable session identifiers, change the tool and threat model. Reproducible simulation and attacker-resistant unpredictability are different requirements.
How should a test isolate random-state consumption?
Give each test its own initialization rather than relying on the order in which a test runner executes cases. A test that passes alone and fails in the suite may be consuming shared state advanced by an earlier case. Reordering tests is a useful diagnostic, not a reason to pick another seed until the failure disappears.
For a stochastic function, test two different contracts. First, identical initialization and inputs should reproduce the specified result. Second, calls using an advancing generator should not accidentally reconstruct the same trial. Use explicit expected behavior; merely asserting that two outputs differ can be flaky when a discrete draw legitimately repeats.
If you compare two estimators using shared simulated inputs, that dependence can be intentional variance reduction. State that design rather than describing every shared random value as a defect. Independence belongs to the statistical unit you defined, not to an aesthetic preference for different-looking arrays.
A strong interview answer ends with the replay boundary
If asked to debug changing results, first locate every state owner. Then inspect repeated construction, process copies, task-to-stream mapping, and changes in draw order. Only after those checks should you attribute the difference to a version or numerical environment.
A concise answer is: “I would inject an explicit generator, allocate streams by stable task identity, and record the seed, bit generator, environment, and call sequence. A retry replays the task; a new replication gets a new identity. I would test both properties independently.”
Write the ownership contract before running the simulation; a repeatable result is useful only when you can explain what it repeats.
Sources and Further Reading
- NumPy: Generator reference
- NumPy: Bit generators
- NumPy: Parallel random number generation
- NumPy: Random compatibility policy
Research note: Official documentation checked October 3–4, 2026. Original replay fixtures ran on Python 3.12.14 and NumPy 2.5.3. They demonstrate the specified state transitions, not cryptographic security or empirical certification of independent streams.