Use np.random.default_rng() to create a local random Generator for sampling and simulation. Learn how seeds, advancing state and child streams affect reproducibility, then practise with a synthetic demand model.
Seeded outputs below illustrate one environment. NumPy Generator does not guarantee identical output across versions; replay checks compare runs inside the current environment.
Show Table of ContentsCreate a local Generator and call its methods. It owns its random state rather than using the legacy global state. A seed makes a run repeatable under the same environment and sequence of calls. Omitting it requests fresh operating-system entropy.
import numpy as np
from numpy.random import Generator, PCG64, Philox, SeedSequence
from copy import deepcopy
import json
rng = np.random.default_rng(42)
print("BitGenerator:", type(rng.bit_generator).__name__)
print("Integers:", rng.integers(0, 10, size=5))
print("Normal:", np.round(rng.normal(0, 1, size=3), 4))Expected output
BitGenerator: PCG64
Integers: [0 7 6 4 4]
Normal: [ 0.9406 -1.951 -1.3022]Compare two freshly created generators using the same seed. Reproducibility also depends on the BitGenerator, NumPy environment and sequence of methods and arguments. Generator does not promise identical output across all future versions.
a = np.random.default_rng(42)
b = np.random.default_rng(42)
x = a.integers(0, 100, size=6)
y = b.integers(0, 100, size=6)
print(x)
print("Replay matches:", np.array_equal(x, y))
np.testing.assert_array_equal(x, y)Expected output
[ 8 77 65 43 43 85]
Replay matches: TrueRecreating a seeded generator inside a loop repeats its first draw. Construct it once when you want a continuing stream. Additional random calls advance state and can change later results.
restarted = [np.random.default_rng(42).integers(0, 100) for _ in range(4)]
rng = np.random.default_rng(42)
continued = [rng.integers(0, 100) for _ in range(4)]
print("Repeated restart:", restarted)
print("Continuing stream:", continued)
assert len(set(restarted)) == 1Expected output
Repeated restart: [np.int64(8), np.int64(8), np.int64(8), np.int64(8)]
Continuing stream: [np.int64(8), np.int64(77), np.int64(65), np.int64(43)]The lower bound is included and the upper bound is excluded by default. For a six-sided die, use integers(1, 7) or explicitly include six with endpoint=True. A tuple size creates a multidimensional array. See legacy randint() and shape.
rng = np.random.default_rng(10)
rolls = rng.integers(1, 7, size=(2, 3), dtype=np.int32)
inclusive = rng.integers(1, 6, size=5, endpoint=True)
print("Rolls:")
print(rolls)
print("Inclusive upper bound:", inclusive)
assert rolls.shape == (2, 3)
assert ((rolls >= 1) & (rolls <= 6)).all()Expected output
Rolls:
[[5 6 2]
[2 5 5]]
Inclusive upper bound: [4 1 5 4 1]random generates floats from zero inclusive to one exclusive. normal uses loc as the mean and scale as the standard deviation; a small sample need not have exactly those statistics. Other methods include lognormal, gamma, beta and binomial.
rng = np.random.default_rng(101)
uniform = rng.random((2, 3))
normal = rng.normal(loc=10, scale=2, size=(2, 3))
print("Uniform:")
print(np.round(uniform, 4))
print("Normal:")
print(np.round(normal, 4))
print("Poisson counts:", rng.poisson(lam=3.5, size=4))
assert ((uniform >= 0) & (uniform < 1)).all()Expected output
Uniform:
[[0.9435 0.3594 0.7848]
[0.5913 0.2943 0.9227]]
Normal:
[[13.4208 12.1216 11.4153]
[11.3755 8.2729 11.928 ]]
Poisson counts: [1 5 2 7]Probabilities must be nonnegative and sum to one. replace=False selects distinct entries and cannot request more samples than the available population. Repeated labels in the population can still produce repeated labels.
rng = np.random.default_rng(101)
labels = np.array(['A', 'B', 'C'])
print("Weighted selection:", rng.choice(labels, size=5, p=[0.2, 0.5, 0.3]))
ids = np.arange(100, 110)
sample = rng.choice(ids, size=4, replace=False)
print("Distinct IDs:", sample)
assert np.unique(sample).size == 4Expected output
Weighted selection: ['C' 'B' 'C' 'B' 'B']
Distinct IDs: [106 108 107 102]Use permutation when the original record order must stay available. A 2D permutation along axis=0 moves entire rows together, preserving the relationship between fields.
rng = np.random.default_rng(12)
records = np.array([[101, 80], [102, 60], [103, 90], [104, 70]])
shuffled = rng.permutation(records, axis=0)
print("Original:")
print(records)
print("Permuted rows:")
print(shuffled)
assert set(map(tuple, records)) == set(map(tuple, shuffled))
assert not np.shares_memory(records, shuffled)
items = np.arange(5)
rng.shuffle(items)
print("Shuffled in place:", items)Expected output
Original:
[[101 80]
[102 60]
[103 90]
[104 70]]
Permuted rows:
[[102 60]
[103 90]
[104 70]
[101 80]]
Shuffled in place: [1 4 3 0 2]Generator supplies distribution methods; a BitGenerator supplies raw randomness and state. default_rng currently uses PCG64. Explicit PCG64 or Philox records the intended algorithm, but changing the algorithm changes the stream. For very large parallel workloads, consult NumPy guidance on PCG64DXSM and Philox.
rng_pcg = Generator(PCG64(12345))
rng_philox = Generator(Philox(2025))
print("PCG64:", rng_pcg.integers(10, size=4))
print("Philox:", np.round(rng_philox.uniform(size=3), 4))
assert type(rng_pcg.bit_generator).__name__ == 'PCG64'
assert type(rng_philox.bit_generator).__name__ == 'Philox' Expected output
PCG64: [6 2 7 3]
Philox: [0.0697 0.7539 0.4566]Spawn separate child sequences for simulations or workers. They are designed to provide independent streams with very high probability; this is not a mathematical guarantee of no overlap. Reconstruct the root seed and repeat the spawn structure to replay the same assignment.
def make_streams(seed, count):
children = SeedSequence(seed).spawn(count)
return [Generator(PCG64(child)) for child in children]
streams = make_streams(2025, 3)
replay = make_streams(2025, 3)
for i, (rng, again) in enumerate(zip(streams, replay), 1):
values = rng.integers(0, 100, size=3)
print("Stream", i, values)
np.testing.assert_array_equal(values, again.integers(0, 100, size=3))Expected output
Stream 1 [87 99 72]
Stream 2 [53 71 62]
Stream 3 [ 5 72 24]This runnable example simulates four worker jobs without starting processes. Assign child streams to stable job IDs so scheduling changes do not change the assignment. Reusing the same seed independently in every worker repeats the same stream; sharing mutable state also makes results sensitive to call order.
def worker_counts(rng):
return rng.poisson(3, size=4)
root_seed = 123
streams = make_streams(root_seed, 4)
results = [worker_counts(rng) for rng in streams]
replayed = [worker_counts(rng) for rng in make_streams(root_seed, 4)]
for job, (values, expected) in enumerate(zip(results, replayed)):
print("Job", job, values)
np.testing.assert_array_equal(values, expected)Expected output
Job 0 [2 1 2 2]
Job 1 [2 3 1 1]
Job 2 [2 6 2 2]
Job 3 [4 2 3 4]A seed restarts from the beginning. A state snapshot resumes at its saved position when restored into the matching BitGenerator and environment. Take a copy and restore before making the same subsequent calls.
rng = Generator(PCG64(7))
state = deepcopy(rng.bit_generator.state)
a = rng.standard_normal(5)
rng2 = Generator(PCG64())
rng2.bit_generator.state = state
b = rng2.standard_normal(5)
print("Continuation:", np.round(a, 4))
print("Exact match:", np.array_equal(a, b))
np.testing.assert_array_equal(a, b)Expected output
Continuation: [ 0.0012 0.2987 -0.2741 -0.8906 -0.4547]
Exact match: TruePCG64 state can be encoded directly with json. This in-memory round trip is equivalent to writing the payload to a UTF-8 file and reading it later. Other BitGenerators can contain arrays needing extra serialization handling. Record the algorithm and environment with the snapshot. See file I/O for saving generated arrays.
rng = Generator(PCG64(2026))
record = {'numpy_version': np.__version__, 'bit_generator': 'PCG64',
'state': deepcopy(rng.bit_generator.state)}
payload = json.dumps(record)
saved = json.loads(payload)
expected = rng.integers(0, 100, size=5)
restored = Generator(PCG64())
restored.bit_generator.state = saved['state']
actual = restored.integers(0, 100, size=5)
print("Saved algorithm:", saved['bit_generator'])
print("Replay:", actual)
np.testing.assert_array_equal(expected, actual)Expected output
Saved algorithm: PCG64
Replay: [85 17 2 63 36]The API migration changes where state lives and can change the generated sequence. rng.random((2, 3)) takes a tuple shape, unlike np.random.rand(2, 3). The replacements below illustrate the methods; identical seeds do not make legacy and modern outputs identical. Related pages: rand(), randn() and random_sample().
rng = np.random.default_rng(24)
print("rand replacement:", np.round(rng.random(3), 4))
print("randn replacement:", np.round(rng.standard_normal(3), 4))
print("randint replacement:", rng.integers(1, 7, size=3))
print("choice replacement:", rng.choice(['A', 'B'], size=3))Expected output
rand replacement: [0.3303 0.4052 0.5747]
randn replacement: [-0.1871 -0.3395 -0.2277]
randint replacement: [4 6 3]
choice replacement: ['A' 'B' 'B']Passing rng makes the source of randomness visible. The function consumes that supplied stream and does not reset it internally. Independent callers can create separate generators with the same seed to replay a simulation.
def daily_orders(rng, days, mean_orders):
return rng.poisson(lam=mean_orders, size=days)
a = daily_orders(np.random.default_rng(80), 7, 12)
b = daily_orders(np.random.default_rng(80), 7, 12)
print("Synthetic orders:", a)
print("Weekly total:", a.sum())
np.testing.assert_array_equal(a, b)Expected output
Synthetic orders: [15 13 13 15 16 13 13]
Weekly total: 98This synthetic model assumes independent daily Poisson demand with mean 12. Simulate 20000 weeks of seven days and estimate the chance that weekly demand exceeds 90 units. This demonstrates a workflow, not a forecast based on real sales; validate assumptions before applying it to a business.
rng = np.random.default_rng(2026)
demand = rng.poisson(lam=12, size=(20000, 7))
weekly = demand.sum(axis=1)
shortage = weekly > 90
probability = shortage.mean()
standard_error = np.sqrt(probability * (1 - probability) / weekly.size)
print("Simulation shape:", demand.shape)
print("Mean weekly demand:", round(float(weekly.mean()), 2))
print("Estimated shortage probability:", round(float(probability), 4))
print("Monte Carlo standard error:", round(float(standard_error), 4))
assert weekly.shape == (20000,)
assert 0 <= probability <= 1Expected output
Simulation shape: (20000, 7)
Mean weekly demand: 83.77
Estimated shortage probability: 0.2296
Monte Carlo standard error: 0.003Vectorized size arguments usually avoid Python loop overhead. Record the seed, NumPy version, algorithm, call sequence, shapes and model assumptions for an experiment. Do not assume reshaping calls or changing sample sizes preserves every later draw. NumPy random generators are intended for simulation and sampling; use a security-specific generator for passwords or authentication tokens.
1. Spawn three child streams from seed 1234 and draw two Poisson counts with mean 2.5 from each. 2. After five normal draws from seed 77, save state and verify the next five after restoring. 3. Replace rand, randn, randint and choice in a toy script with local Generator methods. 4. Simulate a biased six-sided die with probabilities 0.10, 0.15, 0.20, 0.25, 0.20 and 0.10, then compare frequencies. Finite samples will not exactly match their probabilities.
Recreating the root and spawn tree repeats each child assignment.
streams = make_streams(1234, 3)
replay = make_streams(1234, 3)
for i, (rng, again) in enumerate(zip(streams, replay), 1):
values = rng.poisson(2.5, size=2)
print(i, values)
np.testing.assert_array_equal(values, again.poisson(2.5, size=2))Expected output
1 [4 1]
2 [3 4]
3 [0 2]The snapshot is taken after the first five values, before the continuation.
rng = Generator(PCG64(77))
first = rng.normal(size=5)
state = deepcopy(rng.bit_generator.state)
continuation = rng.normal(size=5)
again = Generator(PCG64())
again.bit_generator.state = state
replayed = again.normal(size=5)
print("First five:", np.round(first, 4))
print("Next five:", np.round(continuation, 4))
print("Exact replay:", np.array_equal(continuation, replayed))
np.testing.assert_array_equal(continuation, replayed)Expected output
First five: [ 0.4278 -0.5708 2.6545 -1.6085 0.6617]
Next five: [-0.1434 -0.3545 1.0664 -1.8179 -0.9847]
Exact replay: TrueUse one Generator instance to produce all four outputs in a defined order.
rng = np.random.default_rng(33)
print("Uniform:", np.round(rng.random((2, 3)), 3))
print("Normal:", np.round(rng.standard_normal(3), 3))
print("Integers:", rng.integers(0, 10, size=3))
print("Labels:", rng.choice(['A', 'B', 'C'], size=3))Expected output
Uniform: [[0.444 0.568 0.908]
[0.254 0.589 0.359]]
Normal: [-0.098 0.62 1.837]
Integers: [6 5 4]
Labels: ['A' 'B' 'A']Count outcomes with bincount(). The differences measure sampling variation; one sample does not establish generator quality.
rng = np.random.default_rng(9)
p = np.array([0.10, 0.15, 0.20, 0.25, 0.20, 0.10])
rolls = rng.choice(np.arange(1, 7), size=10000, p=p)
counts = np.bincount(rolls, minlength=7)[1:]
frequencies = counts / rolls.size
print("Expected:", p)
print("Observed:", np.round(frequencies, 3))
print("Differences:", np.round(frequencies - p, 3))
assert counts.sum() == rolls.size
np.testing.assert_allclose(frequencies.sum(), 1)Expected output
Expected: [0.1 0.15 0.2 0.25 0.2 0.1 ]
Observed: [0.098 0.153 0.196 0.257 0.2 0.096]
Differences: [-0.002 0.003 -0.004 0.007 0. -0.004]Open in Google Colab View on GitHub
All sample data is embedded. Run the examples and practise sampling, state replay and simulation. Save a copy in Drive to keep your changes.
Continue with NumPy tutorials, sum(), axis reduction and file I/O. References: Generator documentation, parallel random generation and compatibility policy.
Author & Instructor at plus2net
I write and maintain practical tutorials on Python, PHP, SQL, JavaScript, HTML, jQuery, and web development at plus2net. The tutorials focus on clear explanations, working examples, and code that readers can test and adapt while learning.