Back to Blog
Python

Python NumPy Random Seed Choice and Random Numbers

python numpy random seed choice and random numbers: Learn how to use np.random.seed() and default_rng() in NumPy, choose seed values, and keep random number generation...

numpyrandom-seedreproducibilityrandom-number-generationpython
Illustration of NumPy random seed initialization showing deterministic random number streams branching from a single seed node.

python numpy random seed choice and random numbers requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

Choosing a random seed in Python NumPy controls whether your random numbers are reproducible across runs. np.random.seed() initializes the global random state used by NumPy's legacy random functions. After calling np.random.seed(42), every subsequent call to np.random.rand(), np.random.randint(), np.random.normal(), and related functions draws from a deterministic sequence that begins at that seed.

import numpy as np np.random.seed(42) print(np.random.rand(3)) np.random.seed(42) print(np.random.rand(3))

Both calls print the same three values because re-seeding resets the generator to the same starting point. Without the second np.random.seed(42) call, the second np.random.rand(3) would return the next three values in the sequence.

The seed value itself is an integer that initializes the internal state of the Mersenne Twister (MT19937) algorithm used by the legacy RandomState. The same seed always produces the same initial state, which is why the sequence is reproducible.

How the Legacy Global State Works

The legacy API stores its state in a module-level RandomState instance. This means:

  • np.random.seed() affects every module that imports NumPy and uses the legacy functions
  • The state advances with each call, so the order of calls matters
  • Any code in the process can change the state, which makes the sequence hard to predict across module boundaries
import numpy as np np.random.seed(7) a = np.random.rand() b = np.random.rand() np.random.seed(7) c = np.random.rand() d = np.random.rand() print(a == c, b == d) # True True

The global nature of this state is the main reason the legacy API is discouraged for new code. A function that calls np.random.rand() internally depends on whatever state the rest of the program has left behind.

Choosing a Seed Value

The numeric value of the seed does not affect the statistical quality of the generated numbers. Any integer in the valid range works equally well. The seed only determines the starting point of the sequence.

Common conventions include:

  • 42 for examples and tutorials
  • 0 when a simple constant is enough
  • 1234 or other short integers for test fixtures

What matters is that the seed is fixed when you need reproducibility. If you run the same script twice with np.random.seed(42), you get the same numbers. If you omit the seed or use np.random.seed() with no argument, the state is initialized from system entropy, and the output differs between runs.

For test code, a fixed seed is usually the right choice because it makes failures reproducible. For production simulations where you want a different result each run, omit the seed or use np.random.default_rng() without arguments.

The Modern Generator API and default_rng()

NumPy's current recommendation is to use np.random.default_rng() instead of the legacy np.random.seed() API. default_rng() returns a Generator instance with its own independent state.

import numpy as np rng = np.random.default_rng(42) print(rng.random(3)) rng2 = np.random.default_rng(42) print(rng2.random(3))

The two Generator instances produce the same sequence because they share the same seed. Unlike the legacy API, the state is not global. Each Generator is independent, so two generators with different seeds produce independent streams.

The default algorithm behind default_rng() is PCG64, which is faster and has better statistical properties than the MT19937 used by the legacy API. The exact performance difference depends on the workload, but the Generator API is the one NumPy maintains for new code.

AspectLegacy np.random.seed()Modern default_rng()
State locationGlobal, sharedPer Generator instance
Default algorithmMT19937PCG64
Call stylenp.random.rand()rng.random()
Thread safetyShared state is unsafeIsolated instances
RecommendationLegacy compatibility onlyNew code

Reproducibility Across Runs and Environments

Reproducibility requires three things to stay constant:

  1. The seed value
  2. The random number generator algorithm
  3. The sequence of calls that advance the state

If any of these change, the output changes. The legacy MT19937 algorithm has been stable across NumPy versions, so np.random.seed(42) followed by the same calls produces the same output in most environments. The Generator API uses PCG64 by default, and NumPy reserves the right to change the default algorithm in future releases. If you need bit-for-bit reproducibility across NumPy versions, pin the NumPy version in your environment.

The call sequence matters more than most developers expect. Inserting a single np.random.rand() call before the value you care about shifts everything after it. This is a common source of subtle test failures when code is refactored.

import numpy as np np.random.seed(1) first = np.random.rand() second = np.random.rand() np.random.seed(1) _ = np.random.rand() # consumes one value changed = np.random.rand() print(first == changed) # False

Common Seed-Related Mistakes

The most frequent mistake is re-seeding inside a loop. Each iteration resets the generator, so every iteration produces the same value.

import numpy as np for i in range(5): np.random.seed(42) print(np.random.rand())

This prints the same number five times. The correct pattern is to seed once before the loop and let the state advance naturally.

import numpy as np np.random.seed(42) for i in range(5): print(np.random.rand())

Another mistake is assuming np.random.seed() affects a Generator created with default_rng(). It does not. The legacy seed only initializes the legacy global RandomState. A Generator created with default_rng(42) has its own state and is unaffected by np.random.seed().

Mixing the two APIs in the same program is also a source of confusion. If you use default_rng() for new code but some dependency still calls np.random.seed() or np.random.rand(), the two systems are completely independent.

Practical Patterns for Tests and Parallel Work

For unit tests, seed the generator at the top of the test and pass it explicitly to the code under test. This keeps the test independent of any global state.

import numpy as np def generate_samples(rng, n): return rng.normal(size=n) rng = np.random.default_rng(42) samples = generate_samples(rng, 100)

For parallel work, create one Generator per worker with a distinct seed. The legacy global state is shared across threads, so two threads calling np.random.rand() concurrently can interfere with each other's sequences. Separate Generator instances avoid this entirely.

import numpy as np workers = [np.random.default_rng(i) for i in range(4)]

If you need to resume a sequence later, you can save and restore the state. The legacy API exposes np.random.get_state() and np.random.set_state(). A Generator exposes its state through rng.bit_generator.state.

import numpy as np rng = np.random.default_rng(42) rng.random(10) state = rng.bit_generator.state rng.random(10) rng.bit_generator.state = state resumed = rng.random(10)

The resumed sequence continues from the saved point, which is useful for checkpointing long simulations or reproducing a specific failure after a crash.

python numpy random seed choice and random numbers: Practica | RYUSLOG DEV