Make Random Simulations Reproducible
Run a repeatable simulation with an independent random generator and validated trial count.
Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons
Video lesson: Make Random Simulations Reproducible
The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube
Jump to a chapter
Read the video transcript
Select a timestamp to watch that moment on YouTube.
- 00:00
Suppose you simulate a hundred rolls and find a rare outcome. You want to reproduce that exact run while debugging. You record a seed, run the program again, and the result changes because another part of the program also used the global random generator. The problem is shared state: one extra draw advances the same sequence. Python's `random.Random` lets us create a separate generator instance for one simulation. Give it a seed, pass it into the code that needs random choices, and a test can replay the same sequence. This is useful for games, experiments, and testing algorithms. It is not suitable for passwords or reset links; our separate `secrets` lesson
- 00:43
handles security-sensitive randomness. We will first reproduce the shared-state bug, then build a tiny dice simulation whose outcome depends only on its inputs. Watch each local Python run and ask what exactly determines the result: the seed, the number of rolls, or a hidden call elsewhere. On the lesson page, you can change all three and check your prediction. We use the same seed twice, but one extra call appears before the second list. That call consumes state from the module- level generator. The dice results therefore begin at a different point in the sequence. This does not mean the seed failed; it means the result also depends on the
- 01:25
history of every draw sharing that generator. A global seed can make a small notebook appear reproducible, but later refactoring or a third-party function can disturb the sequence. You might be tempted to reseed immediately before every roll. That destroys useful variation and can accidentally repeat the same value many times. Instead, give the simulation its own generator. Keep the source of randomness visible in the function signature, so a caller can supply a deterministic generator for a test or a fresh one for an exploratory run. The output lists are concrete evidence of the problem, while the specific numbers
- 02:05
are less important than the fact that the two lists differ after a single extra draw. Every `Random` object owns its own generator state. The first and second instances start with the same seed and receive the same five requests, so their sequences match. The third starts with a different seed and produces a different sequence for this example. Two different seeds do not mathematically guarantee different short sequences, so do not use that last comparison as a universal test of a random generator. It simply helps us see how the seed affects this run. More important, an unrelated call to the module-level `random` functions will not
- 02:47
advance any of these three instances. The `rolls` function accepts a generator instead of reaching out to global state. That makes its dependency clear. When testing, create a new generator with a known seed for each independent run; reusing the same instance means its state continues from the previous call. Reproducibility comes from controlling both the initial state and the order of draws. This function has three pieces of its result under control: the seed, the trial count, and the rule that a roll of five or six counts as high. It creates a dedicated generator each time it runs.
- 03:27
Two calls with identical inputs therefore return the same count, even if other code uses randomness between them. We reject a negative trial count rather than silently treating it as zero. The `sum` expression adds Boolean results because in Python `True` counts as one and `False` as zero. For twenty trials, that is a compact way to count successes. A real simulation might return the individual observations as well as a summary, especially when debugging a surprising case. One count alone does not reveal which rolls produced it. Yet this small function demonstrates the design principle: move the random
- 04:07
generator inside a well-defined boundary, or pass it explicitly, and write down the seed that produced a case you want to replay. When the goal is statistical analysis, use many seeds rather than drawing broad conclusions from one fixed run. The first assertion checks replay: same seed and same number of trials produce the same count. The next checks a basic bound. A hundred trials cannot yield fewer than zero or more than a hundred successes. The zero-trial case should return zero, and a negative count should raise an error. These tests say nothing about whether the dice are fair; that is a separate statistical question.
- 04:48
They also do not prove that the chosen seed is somehow special. The seed is simply a way to reproduce an execution path. Try changing it on the site and compare both the count and individual rolls. Then insert an unrelated call to the global generator before `count_high_rolls`; its result should stay the same because it uses its own instance. Finally, consider a code review question: if the function uses `random.Random`, can its output be used as an authentication token? The answer is no. Reproducibility is the feature here, and unpredictability is a different requirement. Complete the practice task by making the generator dependency visible and testing
- 05:31
one edge case.
Understand the concept
Random-looking results help in games and simulations. For a bug report or test, the same run must be reproducible. random.Random(seed) creates an independent pseudorandom generator whose sequence can be replayed from the seed. This is an advantage for simulation but a liability for credentials: use secrets for those.
Calling random.seed at module level changes the shared generator used by other code in the same Python process. That can make an unrelated test depend on execution order. A local Random instance keeps state inside the function. The website console resets globals between runs, but the copied function should still behave safely inside a larger program.
A simple simulated coin uses rng.random() < 0.5. Each draw is a float from 0.0 inclusive to 1.0 exclusive. Values below 0.5 map to H and the rest to T. A sequence such as HHTHT is merely one outcome, not proof of fairness or bias. Statistical claims need many trials and a defined test.
Implement simulate_flips(seed, count) returning a string of H and T. Reject negative counts and let zero produce an empty string. Checks cover replay, lengths, and unchanged global state. Python documents reproducibility guarantees for Random.random with a compatible seeder; higher-level methods can change implementation across versions.
- A seed replays pseudorandom output
- Local Random instances isolate state
- random() returns values in [0, 1)
- Simulated randomness is not secure randomness
See it step by step
Read the code, predict the output, then compare it with the result.
01. Replay the same first draw
import random
a = random.Random(7)
b = random.Random(7)
print(round(a.random(), 6))
print(round(b.random(), 6))0.323833
0.323833Matching seeds replay the same initial draw.
02. Simulate five flips using one generator
import random
rng = random.Random(7)
print(''.join('H' if rng.random() < 0.5 else 'T' for _ in range(5)))HHTHTEach call advances one local generator, yielding a reproducible sequence.
03. Leave the global generator untouched
import random
before = random.getstate()
random.Random(7).random()
print(random.getstate() == before)TrueA separate instance does not mutate module-level random state.
A closer look
Follow the reasoning, inspect each result, then try the suggested changes in the console below.
Replay a seeded draw on purpose
The two generators in the first run begin with the same seed and print the same rounded first draw. That repeatability is valuable when someone reports a surprising simulation result: a seed lets the next person reproduce the exact sequence. It is also why this generator should never make security credentials.
Record the seed together with a test case, and keep the generator local to the simulation function. The output rounded to six places is for readable demonstration; the generator itself maintains a fuller floating-point state. A single matching draw shows replay, but it does not say anything about whether the simulated process models the real world well.
import random
a = random.Random(7)
b = random.Random(7)
print(round(a.random(), 6))
print(round(b.random(), 6))0.323833
0.323833- Construct two separate Random objects with seed seven.
- Compare their first draws.
- Use the seed to reproduce tests, never as a secret-token strategy.
Advance one generator through several trials
The second example creates one Random instance and asks it for five values. Each draw advances its internal state; values below one half become H, and the rest become T. The displayed sequence HHTHT is reproducible for this seeded run. Recreating the generator inside the loop would restart the same first draw every time.
A five-flip sample is far too small to evaluate fairness. It only demonstrates how a trial maps a pseudorandom number into an outcome. The checked function returns a string with one symbol per requested trial, accepts zero as an empty result, and rejects negative counts before consuming any draws.
import random
rng = random.Random(7)
print(''.join('H' if rng.random() < 0.5 else 'T' for _ in range(5)))HHTHT- Create the seeded generator once.
- Draw repeatedly and map each value to H or T.
- Check string length and boundary trial counts.
Leave unrelated random state alone
The last run saves the module-level random state, draws from a separate Random object, and confirms the global state did not change. This isolation matters in a test suite: if one function reseeds the shared generator, another test may start passing or failing depending on execution order.
Test the public function with the same seed twice and with a different seed, but avoid assuming different seeds must always give different short outputs. Coin-flip strings can coincide by chance. Also verify the module-level state remains unchanged. The point is reproducible, local simulation behavior, not a guarantee that every run looks unique.
import random
before = random.getstate()
random.Random(7).random()
print(random.getstate() == before)True- Capture the shared generator state.
- Draw through an independent instance.
- Assert that the shared state is unchanged.
Try it in Python
Edit the example and run it. Python starts in your browser the first time you click Run.
Python console
Ready to runNeed input()? Add one value per line
Your output appears here.
Need a hint?
Create rng = random.Random(seed) once inside the function and call rng.random() for each flip.
Complete the task and select Check task to verify your code.
Quick quiz
Three questions. You can change your answers and try again.
Typical mistakes
Everyone meets these errors. See what causes them and how to fix them.
Changing the shared generator
random.seed(seed)
value = random.random()rng = random.Random(seed)
value = rng.random()What happens: Other code's results depend on this function's call order.
Keep simulation state local.
Recreating the generator for every trial
for _ in range(count):
value = random.Random(seed).random()rng = random.Random(seed)
for _ in range(count):
value = rng.random()What happens: Every trial repeats the first value.
Create once, then advance that instance.