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#
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:
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:
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
setordictand depends on a specific order — rather than sorting explicitly — can vary between runs unlessPYTHONHASHSEEDis 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#
- Reproducibility — the general scientific principle this article applies to code.
- Pseudorandom number generator and Random seed — the underlying mechanism a seed controls.
- NumPy: Random Generator
— the modern
default_rngAPI used above. - NumPy: Random sampling — the full module, including the legacy global-state API.