How do you make code that draws from Python's random module reproducible in a test?
answer
- The module functions share one hidden object
- Seeding touches process-wide state
- Own your generator instead of the global
- random.Random(seed) passed in as a parameter
- Another caller's draw shifts your sequence
basics
~10 sGive the code its own generator: build random.Random(seed) and pass it in, then draw with rng.random(), rng.choice() and friends. Calling random.seed() instead mutates one process-wide generator that every other caller shares.
solid answer
~40 sThe module-level `random.random()`, `random.choice()` and `random.shuffle()` are bound methods of a single hidden `random.Random` instance created at import, so `random.seed(0)` in a test is global mutation: anything that draws a number between the seed and the assertion shifts the sequence, and test order or a new import can break it. Instead accept an `rng` parameter defaulting to `None`, and pass `random.Random(20260903)` from the test — two tests with their own generators cannot interfere. CPython guarantees that `random()` reproduces the same stream for the same seed, but the derived helpers are not covered: `shuffle()` lost its per-call random-source argument and `sample()` stopped accepting sets in 3.11. Assert properties rather than golden literals where you can, and never use `random` for tokens — that is `secrets`.
code
python · 12 linesimport random
def plan_restarts(stops, attempts, rng=None):
rng = random.Random() if rng is None else rng
return [rng.sample(stops, len(stops)) for _ in range(attempts)]
stops = ["lisbon", "porto", "faro", "braga"]
first = plan_restarts(stops, 2, rng=random.Random(20260903))
second = plan_restarts(stops, 2, rng=random.Random(20260903))
print(first == second)go deeper
Know that the same seed replays the same numbers, and that creating random.Random(seed) and passing it in is safer than calling random.seed(). Be able to say why a test that draws unseeded random values cannot assert on the result.
Explain that the module functions are bound methods of one hidden generator, so seeding it is shared mutable state, and describe the rng parameter pattern that removes the sharing entirely.
Show what the reproducibility guarantee actually covers, why golden literals from shuffle or sample are fragile across upgrades, and how you keep fixed-seed regression tests separate from a varying exploratory run that records its seed.
Own the convention: which parts of a system are allowed nondeterminism at all, how seeds are recorded so a production or CI failure can be replayed, and where the line sits between the random module and the secrets module.
### The module-level functions are one shared object `random.random()`, `random.choice()`, `random.shuffle()` and their siblings are not independent functions. At import time the `random` module constructs a single hidden `random.Random` instance and exports its bound methods under those names. Every caller in the process — your code, the code you imported, a library's import-time initialisation — draws from that one generator and advances its state. That is why `random.seed(0)` inside a test is not the isolation move it looks like: it mutates a process-wide object, and anything that draws a number between your seed call and your assertion shifts the sequence you are about to compare against. Change an unrelated import, or run the tests in a different order, and the "deterministic" expectation moves. ### Own your generator The fix is the ordinary dependency-injection move: create your own generator and pass it in. ```python import random def plan_restarts(stops, attempts, rng=None): rng = random.Random() if rng is None else rng return [rng.sample(stops, len(stops)) for _ in range(attempts)] ``` Production passes nothing; the test passes `random.Random(20260903)` and gets the same restarts every run, no matter what else the process did. Two tests that each construct their own `random.Random` cannot interfere with one another, which is exactly the property `random.seed()` fails to give you. If the code is already written against the module functions, patching the name the module imported is a valid stopgap, but an `rng` parameter is cheaper to reason about and shows up in the signature. ### What the same seed actually guarantees CPython documents a narrow but useful promise: the algorithms and seeding functions may change between versions, but a compatible seeder will always be offered, and `random()` will keep producing the same sequence for the same seed. So a bare stream of `rng.random()` values is stable. The *derived* helpers are not covered by that promise, and they have moved: `random.shuffle()` lost its optional per-call random-source parameter in 3.11, and `random.sample()` stopped accepting sets in 3.11. The lesson for tests is to avoid pinning golden literals produced by `shuffle`, `sample` or `choices` across interpreter upgrades. Assert the properties you actually care about — the permutation contains every input exactly once, the chosen index is in range, two runs with the same seed match each other — and reserve exact literals for the same interpreter that produced them. ### Seeds you can read An integer seed is the usual choice, but `random.Random` also accepts `str`, `bytes` and `bytearray` seeds, hashed internally, which lets a test seed itself from a meaningful label. When a test deliberately generates a fresh seed to widen coverage, print or attach it on failure: an unreproducible random failure is worse than no coverage at all. ### The traps `random.SystemRandom` draws from the operating system entropy source, so seeding it does nothing — its `seed()` is a no-op and its output can never be reproduced. That is a feature where you want unpredictability and a silent defeat where you wanted determinism. In the other direction, the Mersenne Twister behind `random` is not cryptographically secure: given enough consecutive outputs its future output is predictable, so tokens, password resets and nonces belong to the `secrets` module regardless of how the tests are written. Restoring state with `random.getstate()` and `random.setstate()` is occasionally proposed as an isolation mechanism for the global generator. It works, but it is a manual save/restore around shared mutable state — the same shape of problem an injected generator removes entirely. ### Determinism is not the same as coverage A single fixed seed makes a test repeatable; it does not make it thorough. A route-planning heuristic that only ever sees the restart order produced by seed 20260903 is tested against one sample of its input space. The mature pattern is a fixed seed for the regression tests that must never flap, plus a separate, deliberately-varying run that records its seed so any failure it finds can be turned into a fixed-seed regression test. Keep the two kinds of test apart: mixing them produces a suite that is neither reliably green nor genuinely exploratory.
- Does the same seed guarantee the same numbers on a different Python version?Only for the core stream. CPython promises that a compatible seeder will always exist and that `random()` keeps producing the same sequence for a given seed; it explicitly does not promise the derived helpers. `shuffle()` and `sample()` both changed in 3.11, so literals recorded from them can differ after an upgrade. Pin exact values only against the interpreter that produced them, and otherwise assert properties: same seed twice matches, the permutation contains every input once.
- Why is seeding random.SystemRandom pointless?`random.SystemRandom` draws from the operating system entropy source rather than an internal state, so its `seed()` does nothing and its output can never be reproduced. That is exactly what you want for unpredictability and a silent defeat when you wanted determinism. It also underlines the security point: the ordinary `random` generator is not cryptographically secure, so tokens and nonces belong to the `secrets` module.
- When is a fixed seed the wrong tool for the test?When the point is exploration rather than regression. One fixed seed proves the code handles one sample of its input space, not that it is correct. Keep fixed-seed tests for the behaviours that must never flap, and run a separate deliberately-varying pass that records its seed so any failure it finds converts into a new fixed-seed regression test. Mixing the two gives a suite that is neither reliably green nor genuinely exploratory.
saying these in an interview costs you the question
- Calls random.seed() in a test and assumes isolation
- Believes seeded output is identical on every Python version
- Uses the random module for tokens or passwords
- Thinks random.Random(0) shares state with the module functions
- Tries to seed random.SystemRandom for reproducibility
- Treats one fixed seed as thorough coverage