Reproducibility: Seeds and Determinism

Why the same script can produce different results on two runs, seeding every random source a pipeline actually uses instead of just one of them, and what a fixed seed does not guarantee on its own.

Reproducibility — the same input producing the same output, run after run — is not automatic in a data or ML script the way it is in a pure function. Anything that samples, splits, shuffles, or initializes randomly introduces a pseudorandom number generator into the process, and a PRNG that starts from a different state produces a different, but equally "correct," sequence of numbers every time.

A seed fixes the sequence, not the outcome's correctness#

Python
import random
random.seed(42)sample = random.sample(range(1000), k=10)

random.seed(42) does not make random.sample return a better or more representative sample — it makes it return the same sample every time this code runs. That is the entire value of a seed: turning "some random result" into "this specific, reproducible result," so a second run can be compared against the first at all.

One seed does not cover every random source#

A script that mixes Python's built-in random module with NumPy almost always needs both seeded, because they are entirely independent generators with independent state:

Python
import randomimport numpy as np
random.seed(42)np.random.seed(42)

Seeding one has no effect on the other. A pipeline that shuffles a list with random.shuffle and also splits an array with a NumPy-based function needs both lines above, not one — a surprisingly common source of "I set the seed and it's still not reproducible" reports.

Prefer an explicit Generator over global seeding#

np.random.seed(42) sets global state shared by every piece of code in the process that calls np.random.* — including library code the caller does not control, which can quietly consume random draws and shift the sequence everything after it sees. NumPy's modern API avoids this by handing back an explicit generator object instead:

Python
rng = np.random.default_rng(seed=42)sample = rng.choice(1000, size=10, replace=False)

rng is a self-contained Generator instance, not global state — passing it explicitly into every function that needs randomness means two unrelated parts of a pipeline can each hold their own generator without one's random draws shifting the other's sequence.

What a seed does not make deterministic#

Seeding is necessary for reproducibility but not sufficient on its own:

  • Floating-point summation order. Parallel or multi-threaded reductions can sum the same values in a different order across runs, and floating-point addition is not perfectly associative — the result can differ in the last few bits even with every random source seeded identically.
  • Hash-dependent iteration order. Python randomizes string hashing by process by default; code that iterates a set or dict and depends on a specific order — rather than sorting explicitly — can vary between runs unless PYTHONHASHSEED is also fixed.
  • Library version differences. A seeded random sample from one version of NumPy is not guaranteed to match the same call on a different version; the algorithm behind a given distribution can change between releases even when the public API does not.

Pin the environment, not just the seed

A seed reproduces the random choices a run makes. Reproducing the result of the run also depends on the same library versions and the same Python version being in use — the same discipline Configuration and Secrets Management covers for a pipeline's settings applies here too: pin what needs to stay fixed, explicitly, rather than trusting today's environment to still exist unchanged next month.

Common mistakes#

Seeding only the library that happens to be top of mind#

A script written primarily in Pandas/NumPy that also calls random.shuffle somewhere — often in a small utility function written earlier — leaves that one call unseeded, and only that one part of the output varies between runs.

Relying on global np.random.seed in shared or library code#

A global seed set inside a function that other code also calls into changes the random sequence for everything that runs after it in the same process — a library that reseeds NumPy internally can silently break reproducibility in the code that imports it. An explicit Generator avoids this entirely.

Treating a seeded run as identical across machines or versions#

A fixed seed reproduces a run given the same software stack. Comparing seeded output across two different NumPy versions, or two different CPU architectures in unusual numerical edge cases, is not a valid reproducibility check on its own.

Further reading#

See also#