Skip to main content

Legacy Random API

NumPy maintains the legacy RandomState class and module-level functions for backward compatibility. However, new code should use Generator and default_rng().
The legacy API is maintained for backward compatibility but will not receive new features. Use Generator for new code.

Why Migrate to Generator?

Better Algorithms

Generator uses PCG64, which is faster and has better statistical properties than MT19937

More Flexibility

Multiple independent streams, better parallelization support

Active Development

New distributions and features added to Generator only

Modern Interface

Cleaner API design, explicit state management

RandomState Class

class numpy.random.RandomState(seed=None)
Legacy random number generator using MT19937 algorithm. Parameters:
  • seed : , optional
    • None: Random seed from OS
    • int: Seed value (0 to 2^32 - 1)
    • array_like: Array of integers for seeding
Example:
import numpy as np

# Create RandomState instance
rng = np.random.RandomState(42)

# Generate random numbers
print(rng.random(5))
# [0.37454012 0.95071431 0.73199394 0.59865848 0.15601864]

print(rng.randint(0, 10, size=5))
# [6 3 7 4 6]

print(rng.normal(0, 1, size=5))
# [ 0.64768854  1.52302986 -0.23415337 -0.23413696  1.57921282]

Migration Guide

Basic Pattern

# OLD: Legacy API with global state
import numpy as np
np.random.seed(42)
x = np.random.random(10)
y = np.random.normal(0, 1, 100)

# NEW: Generator with explicit state
import numpy as np
rng = np.random.default_rng(42)
x = rng.random(10)
y = rng.normal(0, 1, 100)

Method Name Changes

Some methods have been renamed for clarity:
Legacy (RandomState)Modern (Generator)Notes
rand(d0, d1, ...)random((d0, d1, ...))Size as tuple
randn(d0, d1, ...)standard_normal((d0, d1, ...))Size as tuple
randint(low, high)integers(low, high)Renamed
random_sample()random()Same
random_integers()❌ DeprecatedUse integers(..., endpoint=True)
Example:
import numpy as np

# Legacy
rs = np.random.RandomState(42)
legacy_uniform = rs.rand(3, 4)  # Shape as arguments
legacy_normal = rs.randn(3, 4)
legacy_int = rs.randint(0, 10, size=(3, 4))

# Modern
rng = np.random.default_rng(42)
modern_uniform = rng.random((3, 4))  # Shape as tuple
modern_normal = rng.standard_normal((3, 4))
modern_int = rng.integers(0, 10, size=(3, 4))

Global Random Functions

Legacy module-level functions use a single global RandomState instance:
import numpy as np

# These all use the same global RandomState
np.random.seed(42)
x = np.random.random(5)
y = np.random.normal(0, 1, 5)
z = np.random.randint(0, 10, 5)
Global functions share state across your entire program, making debugging and testing difficult. Use explicit Generator instances instead.

Available Global Functions

Simple Random Data:
np.random.random(size=None)      # Uniform [0, 1)
np.random.rand(d0, d1, ...)      # Uniform [0, 1), shape as args
np.random.randn(d0, d1, ...)     # Standard normal, shape as args
np.random.randint(low, high=None, size=None)
np.random.random_sample(size=None)
Permutations:
np.random.shuffle(x)             # Shuffle in-place
np.random.permutation(x)         # Shuffled copy
np.random.choice(a, size=None, replace=True, p=None)
Distributions:
np.random.normal(loc=0, scale=1, size=None)
np.random.uniform(low=0, high=1, size=None)
np.random.exponential(scale=1, size=None)
np.random.binomial(n, p, size=None)
np.random.poisson(lam=1, size=None)
# ... and many more

Seed Management

np.random.seed(seed=None)
Seed the global RandomState. Example:
import numpy as np

# Set seed for reproducibility
np.random.seed(42)
print(np.random.random(3))
# [0.37454012 0.95071431 0.73199394]

# Reset to same seed
np.random.seed(42)
print(np.random.random(3))
# [0.37454012 0.95071431 0.73199394]  # Same values

State Management

state = np.random.get_state()    # Save state
np.random.set_state(state)       # Restore state
Example:
import numpy as np

np.random.seed(42)
print("Before:", np.random.random(3))

# Save state
state = np.random.get_state()

print("Middle:", np.random.random(3))

# Restore state
np.random.set_state(state)
print("After:", np.random.random(3))  # Same as "Middle"

Complete Migration Examples

Example 1: Basic Random Generation

import numpy as np

# === OLD WAY ===
np.random.seed(42)
old_uniform = np.random.rand(5, 3)
old_normal = np.random.randn(5, 3)
old_ints = np.random.randint(0, 100, size=(5, 3))

# === NEW WAY ===
rng = np.random.default_rng(42)
new_uniform = rng.random((5, 3))
new_normal = rng.standard_normal((5, 3))
new_ints = rng.integers(0, 100, size=(5, 3))

Example 2: Monte Carlo Simulation

import numpy as np

def monte_carlo_old(n_simulations):
    """Legacy approach."""
    np.random.seed(42)
    results = []
    for _ in range(n_simulations):
        x = np.random.normal(100, 15, size=1000)
        results.append(x.mean())
    return np.array(results)

def monte_carlo_new(n_simulations):
    """Modern approach."""
    rng = np.random.default_rng(42)
    results = []
    for _ in range(n_simulations):
        x = rng.normal(100, 15, size=1000)
        results.append(x.mean())
    return np.array(results)

# Both produce same statistical results
old_results = monte_carlo_old(100)
new_results = monte_carlo_new(100)

print(f"Old mean: {old_results.mean():.2f}")
print(f"New mean: {new_results.mean():.2f}")

Example 3: Shuffling

import numpy as np

data = np.arange(10)

# === OLD WAY ===
np.random.seed(42)
old_shuffled = data.copy()
np.random.shuffle(old_shuffled)

# === NEW WAY ===
rng = np.random.default_rng(42)
new_shuffled = data.copy()
rng.shuffle(new_shuffled)

print(f"Old: {old_shuffled}")
print(f"New: {new_shuffled}")

Example 4: Multiple Independent Streams

import numpy as np

# === OLD WAY (Problematic) ===
def simulation_old(seed):
    np.random.seed(seed)
    return np.random.normal(size=1000).mean()

# Each call reseeds the global state!
result1 = simulation_old(42)
result2 = simulation_old(43)

# === NEW WAY (Better) ===
def simulation_new(rng):
    return rng.normal(size=1000).mean()

# Create independent generators
rng_parent = np.random.default_rng(42)
rng1, rng2 = rng_parent.spawn(2)

result1 = simulation_new(rng1)
result2 = simulation_new(rng2)

# Or use SeedSequence
from numpy.random import SeedSequence, Generator, PCG64

ss = SeedSequence(42)
child_seeds = ss.spawn(2)
rngs = [np.random.default_rng(s) for s in child_seeds]

results = [simulation_new(rng) for rng in rngs]

Performance Comparison

import numpy as np
import time

# Legacy RandomState (MT19937)
rs = np.random.RandomState(42)
start = time.time()
for _ in range(100):
    rs.normal(0, 1, size=100000)
legacy_time = time.time() - start

# Modern Generator (PCG64)
rng = np.random.default_rng(42)
start = time.time()
for _ in range(100):
    rng.normal(0, 1, size=100000)
modern_time = time.time() - start

print(f"Legacy: {legacy_time:.3f}s")
print(f"Modern: {modern_time:.3f}s")
print(f"Speedup: {legacy_time/modern_time:.2f}x")

Compatibility Notes

Same Seeds, Different Sequences

Generator and RandomState with the same seed produce different random sequences, even when both use MT19937.
import numpy as np
from numpy.random import Generator, MT19937, RandomState

# Same seed, different results
rs = RandomState(42)
rng = Generator(MT19937(42))

print("RandomState:", rs.random(3))
print("Generator:  ", rng.random(3))
# Different values!

Testing Legacy Code

If you need to test code using global functions:
import numpy as np
import pytest

def test_with_random():
    # Save original state
    state = np.random.get_state()
    
    try:
        # Your test with seeded random
        np.random.seed(42)
        result = function_using_random()
        assert result == expected
    finally:
        # Restore state
        np.random.set_state(state)
Better approach - refactor to accept RNG:
import numpy as np

def function_with_rng(rng=None):
    if rng is None:
        rng = np.random.default_rng()
    return rng.normal(size=100)

def test_function():
    rng = np.random.default_rng(42)
    result = function_with_rng(rng)
    assert result.shape == (100,)

Deprecated Functions

These legacy functions are deprecated:
# DEPRECATED
np.random.random_integers(low, high, size)  # Use integers(..., endpoint=True)
np.random.ranf(size)                        # Use random()
np.random.sample(size)                      # Use random()

Summary: Should You Migrate?

Yes, migrate if:
  • Starting new project
  • Need better performance
  • Want parallel random streams
  • Using modern NumPy features
Maybe wait if:
  • Legacy code that works fine
  • Need exact reproducibility with old seeds
  • Tight deadlines
Migration checklist:
  • Replace np.random.seed() with rng = np.random.default_rng(seed)
  • Replace np.random.rand() with rng.random()
  • Replace np.random.randn() with rng.standard_normal()
  • Replace np.random.randint() with rng.integers()
  • Pass rng to functions instead of using global state
  • Use rng.spawn() for parallel streams
  • Update tests to use explicit RNG

See Also

Build docs developers (and LLMs) love