Skip to content

Random.new(42) gives the same numbers every run

Level: 201 · anyone who needs a test, a simulation or a shuffle to be repeatable

One line: A seeded Random is a function of its seed — Random.new(42).rand(100) is 51 on every run and every machine — and shuffle, sample, srand and Kernel#rand all draw from one; only SecureRandom and Random.new_seed are unpredictable. Python's random.Random(42) is just as repeatable, but the same small seed gives a different stream, and a wide seed gives the identical one.

Random.new(seed) builds a Mersenne Twister generator, and every value it produces is determined by the seed and the calls made so far: rand(100) for an Integer below 100, rand for a Float in [0, 1), rand(1..6) for an inclusive range, bytes for raw bytes. That makes seeded output safe to record as an answer key, which is what this page's output is. Array#shuffle and Array#sample take a generator as random:; without one they use the process-wide generator that Kernel#rand uses and that srand(seed) reseeds, so srand(42) followed by rand(10) is as repeatable as an explicit Random. Two things are unpredictable on purpose: Random.new_seed, which asks the operating system for entropy, and SecureRandom, which never takes a seed and is the right tool for tokens and passwords; the program prints only their shape. The old constant Random::DEFAULT was removed in Ruby 3.2 (Not machine-checked here.), and row 12 shows it is a NameError today.

Verified output of random_with_a_seed_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 1. Random.new(42).rand(100)                     -> 51                             an Integer in 0...100 from a seeded generator
 2. Random.new(42), then 5 x rand(100)           -> [51, 92, 14, 71, 60]           the same seed always starts the same stream
 3. Random.new(42), then 3 x rand                -> [0.3745401188473625, 0.9507143064099162, 0.7319939418114051] rand with no argument is a Float in [0, 1)
 4. Random.new(42), then 10 x rand(1..6)         -> [4, 5, 3, 5, 5, 2, 3, 3, 3, 5] a Range is inclusive: dice
 5. srand(42); [rand(10), rand(10)]              -> [6, 3]                         srand seeds the process-wide generator behind Kernel#rand
 6. srand(42) again; [rand(10), rand(10)]        -> [6, 3]                         reseeding restarts the stream; that srand returned the previous seed, 42
 7. [1, 2, 3, 4, 5].shuffle(random: Random.new(1)) -> [3, 2, 5, 1, 4]                shuffle and sample take their generator as a keyword
 8. [1, 2, 3, 4, 5].sample(2, random: Random.new(1)) -> [4, 1]                         sample never repeats an element
 9. Random.new(42) == Random.new(42)             -> true                           same seed, same position: equal; Random.new(42).seed is 42
10. Random.new(42).rand(2 ** 70)                 -> 715669899378304353116          any size of Integer
11. Random.new(42).rand(0)                       -> ArgumentError: invalid argument - 0 an empty range raises; Kernel#rand(0) is the exception, a Float
12. Random::DEFAULT                              -> NameError: uninitialized constant Random::DEFAULT gone; Random.rand and Kernel#rand use the process-wide generator
13. Random.new_seed.class                        -> Integer                        a fresh seed from the OS on each call: new_seed != new_seed is true
14. SecureRandom.hex(8).size                     -> 16                             unpredictable and unseedable, so only its shape is printed; uuid.size is 36
15. Random.new(2 ** 70).rand(2 ** 32), and .rand -> 999817988, 0.2327882718301838  a seed wider than 32 bits loads MT19937 the same way in both languages: Python prints these two numbers too

Reading the output

Rows 1–4 are one seed, four kinds of value. 51 is the first rand(100) of seed 42, and starting over from Random.new(42) gives the same five Integers, the same three Floats and the same ten dice throws — the stream depends on the seed and on how much has been drawn, never on the clock. Rows 5 and 6 are the process-wide generator: srand(42) makes Kernel#rand repeatable in the same way, a second srand(42) restarts the stream, and srand returns the seed it replaced. Rows 7 and 8 pass a generator to shuffle and sample, and the results are as fixed as the seed.

Rows 9–12 are the edges. Two generators built from the same seed are == as long as they have drawn the same amount; rand accepts any Integer bound, 2 ** 70 included; rand(0) on a Random is an ArgumentError — an empty range — while Kernel#rand(0) is the one exception, returning a Float as if no argument had been given; and Random::DEFAULT is gone. Rows 13 and 14 are the sources you cannot seed: Random.new_seed is a fresh Integer from the OS on every call, and SecureRandom.hex(8) is sixteen hex characters whose value the page cannot print because it will never repeat. Row 15 is the cross-language surprise: seeded with 2 ** 70, Ruby's first rand(2 ** 32) and first rand are 999817988 and 0.2327882718301838 — and the Python twin prints exactly the same two numbers.

Compared with Python

random.Random(42) repeats its stream just as faithfully, but for seed 42 it is a different stream: rows 1–4 print different numbers. Row 15 shows that this is not a different algorithm: both languages run MT19937, and for a seed wider than 32 bits they load it identically and produce the identical stream — the same 32-bit word, the same 53-bit float. The difference is only in how a seed that fits in one 32-bit word is loaded — Ruby hands it to the generator's single-word initialiser and CPython always uses the array initialiser (Not machine-checked here.) — so 42 does not port between the two languages while 2 ** 70 does. Everything else maps: randrange(100) for rand(100), random() for rand, randint(1, 6) (inclusive, like a Range) for rand(1..6), random.seed(42) for srand(42) — except that seed() returns None rather than the previous seed — and getrandbits(70) for rand(2 ** 70). shuffle is a method of the generator, shuffles in place and returns None, where Ruby's returns a new array and takes the generator as a keyword. Random objects have no ==; compare getstate(). randrange(0) raises ValueError. There is no random.DEFAULT either: the module-level functions are bound methods of one hidden Random instance. And os.urandom and secrets are the unseedable sources, printed by shape only, as in Ruby.

Verified output of random_with_a_seed_py.py — regenerated by tools/run_examples.py, never hand-typed.

 1. random.Random(42).randrange(100)             -> 81                             an int in range(100) from a seeded generator
 2. Random(42), then 5 x randrange(100)          -> [81, 14, 3, 94, 35]            the same seed always starts the same stream
 3. Random(42), then 3 x random()                -> [0.6394267984578837, 0.025010755222666936, 0.27502931836911926] random() is a float in [0, 1)
 4. Random(42), then 10 x randint(1, 6)          -> [6, 1, 1, 6, 3, 2, 2, 2, 6, 1] randint is inclusive: dice; randrange(1, 7) is the half-open spelling
 5. random.seed(42); [randrange(10), randrange(10)] -> [1, 0]                         seed() seeds the hidden module-level Random behind random.randrange
 6. random.seed(42) again; the same two          -> [1, 0]                         reseeding restarts the stream; seed() returned None, not the old seed
 7. xs = [1, 2, 3, 4, 5]; Random(1).shuffle(xs)  -> [3, 4, 5, 1, 2]                shuffle is in place and returns None; the generator is the receiver
 8. Random(1).sample([1, 2, 3, 4, 5], 2)         -> [2, 1]                         sample never repeats an element
 9. Random(42).getstate() == Random(42).getstate() -> True                           no == on Random objects; compare their states
10. Random(42).getrandbits(70)                   -> 20500439928067422621           any size of int
11. Random(42).randrange(0)                      -> ValueError                     an empty range raises here too; random() takes no bound at all
12. random.DEFAULT                               -> AttributeError                 no such name; the module functions are methods of one hidden Random
13. type(os.urandom(4)).__name__                 -> 'bytes'                        fresh bytes from the OS on each call: os.urandom(4) != os.urandom(4) is True
14. len(secrets.token_hex(8))                    -> 16                             unpredictable and unseedable, so only its shape is printed; len(str(uuid.uuid4())) is 36
15. Random(2 ** 70).getrandbits(32), and .random() -> 999817988, 0.2327882718301838  the same two numbers as Ruby: a seed wider than 32 bits loads MT19937 identically, and 42 does not

The Cryptography library's random is not secrets ↗ is the page on why a seeded generator must never make a token: given enough of its output, the rest of a Mersenne Twister's stream can be reconstructed.

Try it

  1. Roll two dice 1000 times with Random.new(2024) and print a tally of the sums from 2 to 12 as a small histogram, then the most common sum and the mean. Run it twice: the output must not change.
  2. Row 15 shows the two languages agreeing for the seed 2 ** 70. Compare Random.new(2 ** 70).rand(2 ** 64) with random.Random(2 ** 70).getrandbits(64), and Random.new(42).bytes(4) with random.Random(42).randbytes(4). The 32-bit values agreed, so what do the two disagreements tell you about how each language assembles a 64-bit number from 32-bit words, and how it loads a one-word seed?
Solution to 1

random_with_a_seed_kata_rb.rb in full — pasted here by tools/run_examples.py from the file CI runs.

# Kata: roll two dice 1000 times with a seeded generator and tally the sums.
# Seeded, the tally is the same on every run, so an answer key can hold it.

r = Random.new(2024)
sums = Array.new(1000) { r.rand(1..6) + r.rand(1..6) }
tally = sums.tally.sort.to_h
tally.each { |sum, count| puts format("%2d %-4d %s", sum, count, "#" * (count / 5)) }
most = tally.max_by { |_, count| count }
puts "most common: #{most[0]} (#{most[1]} times); mean #{sums.sum.fdiv(sums.size)}"

Verified output of random_with_a_seed_kata_rb.rb — regenerated by tools/run_examples.py, never hand-typed.

 2 20   ####
 3 54   ##########
 4 79   ###############
 5 117  #######################
 6 121  ########################
 7 190  ######################################
 8 144  ############################
 9 126  #########################
10 85   #################
11 48   #########
12 16   ###
most common: 7 (190 times); mean 7.015

See also