skip to content

In Ruby, how do Kernel#rand, Random.rand and Random.new(seed) differ, and how do you make random results reproducible?

level: middleimportance: should knowfreq 30%

answer

  1. one default generator, many instances
  2. rand(1.5) is rand(1)
  3. Random#rand(0): ArgumentError
  4. same seed, same sequence
  5. Mersenne Twister: not for secrets

basics

~20 s

Kernel#rand and Random.rand draw from Ruby's shared default generator, while Random.new(seed) creates an independent one. The same seed replays the same sequence, and shuffle and sample accept random: for repeatable results. None of them is fit for secrets.

solid answer

~40 s

`rand` with no argument returns a Float in `0.0...1.0`, `rand(6)` an Integer from 0 to 5, and `rand(1..6)` a member of the range. `Kernel#rand` converts its limit with `to_i` and `abs`, so `rand(1.5)` is always `0` and `rand(0)` returns a Float, while `Random.rand(1.5)` returns a Float below 1.5 and `Random.rand(0)` or `Random.new.rand(0)` raise `ArgumentError`. `srand(seed)` reseeds the shared default generator for the whole process and returns the previous seed; `Random.new(42)` gives an independent generator whose sequence repeats for the same seed, and `shuffle(random:)` and `sample(random:)` take one. The generator is a Mersenne Twister, so tokens and passwords belong to `SecureRandom`. `Random::DEFAULT` was removed in Ruby 3.2.

code

ruby · 12 lines
ruby
rand(1..6)          # => an Integer from 1 to 6
rand(1.5)           # => 0 (limit.to_i is 1)
Random.rand(1.5)    # => a Float below 1.5

a = Random.new(42)
b = Random.new(42)
[a.rand(100), a.rand(100)] == [b.rand(100), b.rand(100)] # => true

deck = (1..10).to_a
deck.shuffle(random: Random.new(7)) == deck.shuffle(random: Random.new(7)) # => true

Random.new.rand(0)  # ArgumentError: invalid argument - 0

go deeper

for a junior

Know rand, rand(n) and rand(range), and that Random.new(seed) gives the same numbers every run for the same seed.

for a middle

Explain the shared default generator versus instances, the Kernel versus Random argument rules, and how random: makes shuffle and sample repeatable.

for a senior

Inject seeded generators so tests are repeatable without srand, log seeds for replay, and keep Random away from anything security-sensitive.

for a principal

Set the team rule: randomness is a dependency passed in explicitly, reproducible in tests and cryptographic wherever an attacker could benefit.

## One default generator, many instances Ruby's **`Random`** class wraps a pseudo-random number generator (PRNG): a deterministic sequence that only looks random. There are two ways to use it: - The **default generator**, shared by the whole process, behind `Kernel#rand`, `Kernel#srand`, `Random.rand`, `Random.srand` and the default `random:` of `Array#shuffle` and `Array#sample`. - **Independent instances** created with `Random.new` or `Random.new(seed)`, each with its own state. Drawing from one does not advance the others. `Random::DEFAULT`, once the handle to the shared generator, was deprecated in Ruby 3.0 and removed in 3.2; in Ruby 4.0 use the class methods instead. ## Arguments and return values | Call | Returns | |---|---| | `rand`, `Random.rand` | Float in `0.0...1.0` | | `rand(6)`, `Random.rand(6)` | Integer from 0 to 5 | | `rand(1..6)`, `Random.new.rand(1..6)` | Integer from 1 to 6 (range member) | | `rand(1.5)` | always `0` - Kernel converts the limit with `to_i` | | `Random.rand(1.5)`, `Random.new.rand(1.5)` | Float in `0.0...1.5` | | `rand(0)` | Float in `0.0...1.0` | | `Random.rand(0)`, `Random.new.rand(0)` | raises `ArgumentError` (invalid argument - 0) | | `rand(-100)` | Integer below 100 - Kernel uses the absolute value | The Kernel version is the forgiving one: it truncates the limit and takes its absolute value. `Random#rand` and `Random.rand` are strict and treat a Float limit as a Float range. ## Making results reproducible 1. **Seed an instance** and pass it around: `rng = Random.new(42)`. Two generators built with the same seed produce the same sequence, and `rng.seed` returns the seed for logging. 2. **Pass the generator to library calls**: `list.shuffle(random: rng)` and `list.sample(3, random: rng)` both accept `random:`. 3. **Inject it** into the code under test (a keyword argument with a `Random.new` default) so production stays unpredictable while tests are repeatable. 4. **Avoid `srand` in library code.** `srand(42)` reseeds the shared generator for every caller in the process and returns the previous seed; a test that calls it silently changes what unrelated code draws next. Random objects can also be marshaled, so a generator's position can be saved and restored. ## Not for secrets The documentation is explicit: `Random` is a **modified Mersenne Twister** with a period of 2 to the 19937th minus one, and it is **not for cryptographic use**. Its future output can be predicted from enough observed output, and a seeded generator is predictable by design. Session tokens, password-reset codes and API keys come from `SecureRandom`. ## Common mistakes - **Treating `rand(1.5)` like `Random.rand(1.5)`.** The Kernel version truncates the limit, so a Float limit below 2 always yields `0`. - **Guarding against `rand(0)` differently per call style.** Kernel returns a Float; `Random` raises. Code that switches from `rand(n)` to an injected `rng.rand(n)` can start raising when `n` is zero. - **Seeding the process in a helper.** A `srand` call hidden in a factory or fixture makes every later draw in the suite depend on test order. - **Building a fresh seeded generator per draw.** `Random.new(42).rand(100)` in a loop returns the same number every time, because each generator restarts the sequence. - **Porting old code that names `Random::DEFAULT`.** On Ruby 4.0 it raises `NameError`; replace it with `Random` class methods or an explicit instance. ## What interviewers look for - Knowing that `rand` and `Random.rand` share one generator while `Random.new` does not. - The Kernel-versus-`Random` differences for Float and zero limits. - Seeding an **instance** for reproducible tests rather than reseeding the process. - Naming the security boundary without being asked.

  • In a Ruby test suite, why is calling srand(1234) inside one test risky?
    `srand` reseeds the process-wide default generator that `rand`, `Random.rand`, `shuffle` and `sample` use by default. After that call every later draw in the process is predictable and shared, so unrelated tests become order-dependent on each other's draws. Create a `Random.new(1234)` and pass it to the code under test or to `shuffle(random:)` instead.
  • In Ruby, how do you log enough to replay a random run later?
    Create the generator with an explicit seed, for example `seed = Random.new_seed` and `rng = Random.new(seed)`, log the seed, and pass `rng` everywhere randomness is drawn. Replaying with `Random.new(logged_seed)` reproduces the same sequence as long as the same calls happen in the same order. `rng.seed` returns the seed if it was not kept separately.

saying these in an interview costs you the question

  • srand(42) only affects the code that called it.
  • rand(1.5) returns a Float up to 1.5, just like Random.rand(1.5).
  • Random.new(seed) is fine for session tokens if the seed is kept secret.
  • Random::DEFAULT is how you reach the shared generator in Ruby 4.0.
  • Kernel#rand and Random#rand treat a limit of 0 the same way.