Python Module 6 – Modules, packages and the standard library
Random numbers, simulations and statistics
Generate repeatable random data with seeds, pick and shuffle with weights, estimate odds by simulation and summarise data with the statistics module.
What you will learn
- Generate repeatable random data with seeds and separate Random objects
- Choose, sample and shuffle, with weights and without repeats
- Estimate a probability by simulation and judge how precise the estimate is
- Summarise data with the statistics module, from averages to quartiles
- Choose secrets instead of random for tokens, passwords and codes
Before you start
What you’ll learn · Before you start
What you will learn
- Generate repeatable random data with seeds and separate Random objects
- Choose, sample and shuffle, with weights and without repeats
- Estimate a probability by simulation and judge how precise the estimate is
- Summarise data with the statistics module, from averages to quartiles
- Choose secrets instead of random for tokens, passwords and codes
Before you start
On this page
How likely is at least one six in four rolls of a die? Python answers in a second by playing the game 100,000 times
and counting, and the same tools pick quiz questions, shuffle a deck and split data for a test. They come from the
random module and its pseudo-random: Made by a fixed algorithm that only looks random. The random module's Mersenne Twister turns a seed into such a stream, so anyone who learns the generator's state can work out every number that follows. generator: a fixed algorithm, the Mersenne
Twister, turns a starting value called the seed: The starting value of a pseudo-random generator. The same seed gives the same stream of numbers, so random.seed(42) or random.Random(42) makes a simulation or a test repeatable. into a long stream of numbers that pass for
random, and the same seed always gives the same stream.
You will make runs repeatable, pick with weights and without repeats, measure how far a simulated answer can be from
the truth, and describe the numbers you collect with the statistics module. One rule comes before all of it:
random is made for games, simulations and tests, never for passwords or tokens, which come from the secrets module.
Seeds make a run repeatable
random.random() returns a float from 0.0 up to, but not including, 1.0, and every other function of the module draws
on the same hidden generator. Python seeds that generator from the operating system’s source of randomness when the
module is imported, so two runs of a program normally see different numbers. Call random.seed() with a value of your
own and the stream starts again from a known point:
import random
# The same seed gives the same stream, run after run.
random.seed(42)
first = [random.randint(1, 6) for _ in range(10)]
random.seed(42)
again = [random.randint(1, 6) for _ in range(10)]
print(first)
print("same again:", again == first)
# Any other draw from the shared generator moves the stream along.
random.seed(42)
random.random()
print([random.randint(1, 6) for _ in range(10)])
# A Random object has a generator of its own.
dice = random.Random(42)
coins = random.Random(7)
print([dice.randint(1, 6) for _ in range(10)])
print("".join(coins.choice("HT") for _ in range(10))) Output
[6, 1, 1, 6, 3, 2, 2, 2, 6, 1] same again: True [1, 6, 3, 2, 2, 2, 6, 1, 6, 6] [6, 1, 1, 6, 3, 2, 2, 2, 6, 1] THTHHHTHHH
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 seeds.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
- The first two lines are the whole point of a seed: ten rolls after
seed(42), then exactly the same ten after a secondseed(42). - In the third line, one call to
random()came first. It used up the two internal numbers behind the first two rolls of the first line, so the rolls start where its third roll was. Any code that draws from the module’s shared generator moves the stream along for everyone else. - A
random.Randomobject carries a generator of its own:Random(42)rolls the same dice asseed(42), and nothing outside the object can disturb its stream. Give each simulation or test its ownRandominstead of seeding the module.
Step through the same run as requests to a generator and the rolls it hands back, and watch where each line of rolls begins:
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- Seed the module. random.seed(42) fills the shared generator's state from 42. From now on every function of the module, called from any part of the program, takes its numbers from this one stream.
- Ten rolls. Ten calls to randint(1, 6) take the next numbers of the stream and turn them into rolls, which the program prints as its first line.
- The same seed again. A second seed(42) puts the state back where it started, so the next ten rolls are the same ten, and the program prints same again: True.
- Something else draws first. After a third seed(42), one call to random() comes before the rolls. A float is made from two numbers of the stream, so it takes the two that gave the first two rolls.
- The rolls start later. The ten rolls now begin where the third roll was: 1, 6, 3, 2 … The stream has not changed. Something else drew from it first, as any code that calls the module can.
- A generator of its own. Random(42) fills a state of its own from 42. It rolls 6, 1, 1, 6 … like the first line, and no draw from the shared generator can move it.
The stream never changed; only the point at which the rolls began to read it did. What sits behind the stream is drawn below. The state is 624 numbers of 32 bits, and each output is one of them, scrambled:
How a seed becomes a stream of numbers
Text description of the diagram
The diagram shows, from top to bottom, how the random module produces its numbers.
- A seed, given to random.seed() or to random.Random(), fills the generator's state: 624 numbers of 32 bits each.
- Each draw takes the next number of the state and scrambles it into one 32-bit output. The scrambling step can be undone, which is why the outputs reveal the state.
- random(), randint(), choice(), choices(), sample() and shuffle() are all built on these outputs: random() uses two of them for one float, and randint() and choice() usually one.
- After 624 outputs the generator mixes the whole state again and starts taking numbers from the top.
The same seed fills the state with the same 624 numbers, so the same outputs follow, which is what makes a seeded run repeatable.
That design gives a period of 2¹⁹⁹³⁷ − 1 outputs before the stream repeats, and floats with 53 bits of precision. Seeded streams do not depend on the computer: the example above printed the same rolls on CPython 3.14.8 and on the Python that runs in your browser.
Across Python versions the promise is narrower. The documentation guarantees only that random() keeps producing the
same stream for the same seed; the algorithms behind randint(), choice(), shuffle() and the rest may change in a
new release. A test that compares a seeded shuffle with a stored list can therefore break after an upgrade, while a
test that checks properties of the result (the same items, the right length, a share near the expected one) keeps
passing. Threads are the other catch: the module’s functions share one generator, so in a program with several
threads the order of draws, and with it what each thread sees, can change from run to run.
Key idea
A seed fixes where the stream starts, not who draws from it: give each simulation or test its own
random.Random(seed), and check seeded results by their properties.
Checkpoint
Check: Seeds and the shared generator
choices() can repeat an item, sample() never does
A quiz app makes a good test bench: it needs questions picked at random, the hard ones less often, and no question twice in one quiz.
import random
from collections import Counter
rng = random.Random(5)
bank = {
"easy": ["E1", "E2", "E3", "E4"],
"medium": ["M1", "M2", "M3"],
"hard": ["H1", "H2"],
}
levels = list(bank)
weights = [5, 3, 2] # out of every ten picks: five easy, three medium, two hard
# choices(): k picks with replacement, in proportion to the weights
print(rng.choices(levels, weights=weights, k=10))
counts = Counter(rng.choices(levels, weights=weights, k=10_000))
print({level: f"{counts[level] / 10_000:.1%}" for level in levels})
# sample(): k different items, never the same one twice
every_question = [q for level in levels for q in bank[level]]
print(rng.sample(every_question, k=4))
# shuffle(): reorders the list itself and returns None
order = every_question.copy()
result = rng.shuffle(order)
print(order, result)
# randint() includes both ends; randrange() stops before its end
print(rng.randint(1, 3), rng.randrange(1, 3), round(rng.uniform(0, 10), 3)) Output
['medium', 'medium', 'medium', 'hard', 'medium', 'hard', 'easy', 'easy', 'hard', 'medium']
{'easy': '50.0%', 'medium': '30.1%', 'hard': '19.9%'}
['E3', 'M3', 'M2', 'E2']
['M3', 'E4', 'M1', 'E3', 'E2', 'E1', 'M2', 'H1', 'H2'] None
3 2 5.338
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 weighted_quiz.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
choices(population, weights=…, k=…)makes k independent picks with replacement: Drawing so that every pick can take any item again, as if each one were put back first. random.choices() draws with replacement; random.sample() draws without, using each position at most once., each in proportion to its weight. Ten picks can look nothing like 5 : 3 : 2 (only two of them came out easy), but 10,000 picks land within a few tenths of a percent of 50, 30 and 20 %.cum_weightstakes running totals instead (5, 8, 10), which saves work when you pick from the same weights many times.sample(population, k)picks k different positions, so no question comes up twice. It raisesValueErrorwhen k is larger than the population, and since Python 3.11 it accepts only sequences: passsorted(a_set), not a set. Itscountsargument repeats items without building the longer list:sample(["gold", "silver"], counts=[1, 9], k=3)draws three of ten coins, so at most one of the three is gold.shuffle(x)reorders the list in place: Changing an object itself instead of making a new one. list.sort() sorts a list in place and returns None, while sorted() leaves its input as it was and returns a new list. More in “Lists: create, index, slice and modify” and returnsNone, theNonethe fourth line printed. For a shuffled copy, usesample(x, k=len(x)). For a list of more than 2,080 items the number of possible orders is larger than the generator’s period, so most orders can never come up.randint(1, 3)can return 3, because it includes both ends.randrange(1, 3)cannot: likerange(), it stops before its end.uniform(0, 10)returns a float between the two.
Which call fits depends on three things about your draw. Answer them in the table: it rules out the calls that do not fit, with the reason from the run above.
Answer the questions above: the table then marks what fits your case and says why the rest does not.
| Call | Never the same item twice | Some items more likely | More picks than items | Original left as it was |
|---|---|---|---|---|
| choices(seq, k=n) Typical use: simulated customers, weighted quiz levels | Poor every pick is independent: ten picks drew medium five times | Good weights or cum_weights: 10,000 picks came out 50.0, 30.1 and 19.9 % | Good k may be any size | Good returns a new list |
| sample(seq, k=n) Typical use: a raffle, a quiz without repeats, a test set | Good each position is used once | Mixed no weights, but counts repeats items, as in counts=[1, 9] | Poor k above the length raises ValueError | Good returns a new list |
| shuffle(lst) Typical use: a deck of cards you deal from | Good every item exactly once | Poor no weights | Mixed the list's own items, no more | Poor reorders the list in place and returns None |
| sample(x, k=len(x)) Typical use: a shuffled copy of a list, a tuple or a string | Good every item exactly once | Poor no weights | Mixed exactly len(x) items | Good a new list; works on tuples and strings too |
How the answers decide
- “May an item come up again, as a die can show 6 twice?” No, each item at most once: rules out choices(seq, k=n). Each pick of choices() is independent, so an item can come up twice: ten weighted picks in the quiz drew medium five times.
- “May an item come up again, as a die can show 6 twice?” Yes, every pick is independent: rules out sample(seq, k=n), shuffle(lst) and sample(x, k=len(x)). sample() and the shuffles use every position once, so a die simulated with them could never show 6 twice.
- “Should some items come up more often than others?” Yes, they have weights: rules out shuffle(lst) and sample(x, k=len(x)) and adds a caveat to sample(seq, k=n). Only choices() takes weights. sample() can only repeat items with counts, as counts=[1, 9] does for one gold coin among ten.
- “Must the original sequence keep its order?” Yes, or it is a tuple or a string: rules out shuffle(lst). shuffle() reorders the list itself and returns None, and a tuple or a string cannot be reordered at all; sample(x, k=len(x)) shuffles a copy.
In short: choices() for independent picks, with weights if you need them; sample() for different items;
shuffle() for a list you no longer need in its old order; sample(x, k=len(x)) for a shuffled copy. choice(seq)
is the simple case of one item, for one winner or one move in a game.
Keeping the result of shuffle()
order = rng.shuffle(order) replaces your list with None, because shuffle() reorders the list itself and
returns nothing. Write rng.shuffle(order) on a line of its own, or order = rng.sample(order, k=len(order)) when
you want a new list and the old one unchanged.
The module also draws from distributions: uniform(a, b) gives any value in a range, gauss(mu, sigma) a value on a
bell curve and expovariate() a waiting time, and since Python 3.12 binomialvariate(n, p) gives a whole number, the
successes in n tries. Python 3.13 also added a command line: python -m random 6 prints a whole number from 1 to 6,
and python -m random tea coffee picks one of the words.
Checkpoint
Check: choices(), sample() and shuffle()
A simulation estimates a probability by counting
A simulation answers a probability question by repeating the experiment many times and counting how often the event happened. Take an old gambling question: roll a die four times; how likely is at least one six? Each trial below is one game of four rolls, and the program prints the estimates of four runs, of 100, 1,000, 10,000 and 100,000 games:
import math
import random
def at_least_one_six(rng, rolls=4):
"""One experiment: roll a die four times. Did a six come up?"""
return any(rng.randint(1, 6) == 6 for _ in range(rolls))
exact = 1 - (5 / 6) ** 4
print(f"exact: {exact:.4f}")
rng = random.Random(2)
for trials in (100, 1_000, 10_000, 100_000):
hits = sum(at_least_one_six(rng) for _ in range(trials))
estimate = hits / trials
typical = math.sqrt(exact * (1 - exact) / trials)
print(f"{trials:>7,} trials: {estimate:.4f} off by {abs(estimate - exact):.4f} typical error {typical:.4f}") Output
exact: 0.5177
100 trials: 0.5100 off by 0.0077 typical error 0.0500
1,000 trials: 0.5320 off by 0.0143 typical error 0.0158
10,000 trials: 0.5158 off by 0.0019 typical error 0.0050
100,000 trials: 0.5176 off by 0.0002 typical error 0.0016
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 dice_simulation.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The estimates close in on 0.5177, the exact answer the program prints first. It comes from counting the other way: a game has no six only if all four rolls miss, each with probability 5/6, so the chance of at least one six is 1 − (5/6)⁴. The number of sixes in four rolls follows the binomial distribution: The number of successes in n independent tries that each succeed with probability p. The chance of exactly k successes is C(n, k) × pᵏ × (1 − p)ⁿ⁻ᵏ; the sixes in four rolls of a die follow it with n = 4 and p = 1/6., which the next figure draws for n tries that each succeed with probability p. It starts at the dice game: 4 tries, a chance of 0.166667 (1/6) and at least 1 success.
In 4 tries with p = 0.17, the chance of at least 1 successes is 0.5177; on average there are 0.67.
The numbers as a table
| Successes | Probability |
|---|---|
| 0 | 0.4823 |
| 1 | 0.3858 |
| 2 | 0.1157 |
| 3 | 0.0154 |
The highlighted bars add up to 0.5177, the answer the simulation is aiming at, and the bar at 0 holds the rest, (5/6)⁴ ≈ 0.4823. Where both exist, the exact answer is the best test of the simulation’s code. The simulation earns its place when the exact answer is hard to work out: the same few lines count any rule you can write as code, while a formula has to be found again for every new rule.
Change the game
Set the tries to 24 and the chance to 0.027778, which is 1/36, the chance of a double six when two dice are thrown together. The chance of at least one double six in 24 throws reads 0.4914, just under one half, even though 24 × 1/36 is two thirds.
Checkpoint
Check: Estimating by simulation
Four times the trials halve the error
The estimates in the table are never exact, but their error shrinks in a predictable way. For a probability p estimated from n independent trials, the typical error, called the standard error: The typical distance between an estimate and the true value. For a probability p estimated from n independent trials it is √(p(1 − p) / n), so four times as many trials halve it., is:
In words: the error is largest when p is near one half and shrinks with the square root of the number of trials. For the dice game and 1,000 trials it is √(0.5177 × 0.4823 ÷ 1,000) ≈ 0.0158, the typical error the program printed in that row.
An estimate’s error behaves like a draw from a bell curve centred on 0 whose spread is the standard error, so in about two runs out of three the estimate lands within one standard error of the truth, and in about 19 out of 20 within two. The figure draws the errors of 1,000-trial runs in percentage points, with a spread of 1.58 points and a band of ±1.58 shaded:
For a normal distribution with mean 0 and standard deviation 1.58, the probability of a value between −1.58 and 1.58 is 0.6827 (68.3 %), from z = −1.00 to z = 1.00.
The numbers as a table
| Range | Probability |
|---|---|
| μ ± 1σ | 0.6827 |
| μ ± 2σ | 0.9545 |
| μ ± 3σ | 0.9973 |
The band holds 0.6827 of all runs; widen it to ±3.16 points, two standard errors, and it holds 0.9545. Every row of the program’s table landed within one standard error of its own size: the 1,000-trial run, for one, was off by 1.43 points, inside the band of ±1.58 drawn here.
Ten times the trials
Set σ to 0.5, about the standard error of 10,000 trials, and keep the band at ±1.58. It now holds 0.9984 of all runs: ten times the trials make the error about 3.2 times smaller (√10).
Because n sits under a square root, halving the error takes four times as many trials. Seeding the Random object
makes the whole table repeatable: run it again and every estimate is the same. Another seed gives other estimates,
spread around the exact answer by about the same typical error.
Key idea
A simulated probability is typically off by √(p(1 − p)/n): ten times the trials shrink the error about 3.2 times, and halving it costs four times the trials.
Checkpoint
Check: How precise an estimate is
statistics finds the middle, the spread and the quartiles
The statistics module describes a list of numbers: where its middle is, how spread out it is and how it divides
into quarters. Here are the marks of a class of twenty:
import statistics as st
marks = [72, 85, 64, 91, 58, 77, 85, 69, 94, 81, 66, 85, 73, 88, 47, 79, 70, 83, 61, 90]
print("students ", len(marks))
print("mean ", st.mean(marks))
print("median ", st.median(marks))
print("mode ", st.mode(marks))
print("stdev ", round(st.stdev(marks), 2), " pstdev", round(st.pstdev(marks), 2))
print("quartiles", st.quantiles(marks, n=4))
print("inclusive", st.quantiles(marks, n=4, method="inclusive"))
print("deciles ", st.quantiles(marks, n=10))
grades = ["B", "A", "C", "A", "B"]
print("tied ", st.mode(grades), st.multimode(grades))
# Two tests taken by the same eight students
mid = [55, 62, 70, 48, 81, 66, 74, 90]
final = [58, 70, 72, 52, 85, 63, 80, 94]
print("correlation", round(st.correlation(mid, final), 3))
fit = st.linear_regression(mid, final)
print(f"final = {fit.slope:.3f} × mid + {fit.intercept:.2f}")
for summary, data in ((st.mean, []), (st.stdev, [42])):
try:
summary(data)
except st.StatisticsError as error:
print("StatisticsError:", error) Output
students 20 mean 75.9 median 78.0 mode 85 stdev 12.48 pstdev 12.17 quartiles [66.75, 78.0, 85.0] inclusive [68.25, 78.0, 85.0] deciles [58.3, 64.4, 69.3, 72.4, 78.0, 82.2, 85.0, 87.4, 90.9] tied B ['B', 'A'] correlation 0.974 final = 1.009 × mid + 2.87 StatisticsError: mean requires at least one data point StatisticsError: stdev requires at least two data points
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 marks_summary.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
The figure draws the same twenty marks as dots on one line, with the box between the first and third quartile: One of the three cut points that split sorted data into four groups of the same size; the middle one is the median. statistics.quantiles(data, n=4) returns all three. that the program printed, the median across it and the mean below:
The marks of examples/marks_summary.py, with the program's quartiles and mean
Text description of the diagram
The figure draws the twenty marks of the class as dots on one line from 40 to 100. Marks that are close together are stacked, so the three marks of 85 form a column of three dots.
- Above the dots, a box runs from the first quartile, 66.75, to the third quartile, 85.0. A thick line across the box marks the median, 78.0. About half of the marks lie inside the box: eleven of the twenty, counting the three marks of 85 on its edge.
- Lines reach out from the box to the lowest mark, 47, and the highest, 94.
- A diamond on the line marks the mean, 75.9. It sits to the left of the median because the one low mark, 47, pulls the mean down more than it moves the middle value.
The quartiles are those of statistics.quantiles(marks, n=4) with its default method, "exclusive".
mean()is the average andmedian()the middle value, here 75.9 and 78.0. One low mark, 47, pulls the mean down more than the median, which is why the mean’s diamond sits left of the median’s line, and why medians describe skewed data such as incomes better.mode()is the most common value. With a tie it returns the first of the tied values in the data, so"B"wins here;multimode()returns all of them.stdev()is the sample standard deviation. Its sum of squares is divided by n − 1 rather than n, a correction for the group being only part of everyone you care about;pstdev()divides by n and describes exactly the data you have. Usepstdev()when the class is all you want to describe, andstdev()when it stands for every student who sat the test.quantiles(data, n=4)returns the three cut points that split the data into quarters;n=10gives deciles. The default method,"exclusive", assumes the real population can go beyond your lowest and highest values;"inclusive"treats them as the true minimum and maximum, so its outer quartiles are never further out: here the lower one moves from 66.75 to 68.25.correlation()measures how closely two lists follow a straight line together, from −1 to 1, andlinear_regression()fits that line: here a student’s final mark is about 1.009 times their mid-term mark plus 2.87.- With too few values there is nothing to compute:
mean([])andstdev([42])raiseStatisticsError, a kind ofValueError.
Move one mark
Press Edit, change the mark 47 to 67 and run the program. The mean rises by a whole mark, to 76.9, while the median stays at 78.0: 67 is still below the two middle marks, 77 and 79.
The functions accept ints, floats, Decimal and Fraction values, and they keep exact types exact: the mean of
Fraction values is a Fraction. Mixing types in one list is not supported, so convert first, for example with
map(float, data). The module aims at the level of a graphing or scientific calculator and leaves large-scale work to
libraries such as NumPy and SciPy.
Checkpoint
Check: Averages, spread and quartiles
random is not for secrets
The documentation says it plainly: the generators of random should not be used for security. The stream is
predictable, and not only to someone who knows the seed. Each output is a state number put through a scrambling step
that can be undone, so 624 outputs in a row hand over the whole state. This program watches a generator seeded by the
operating system, whose seed nobody knows, and predicts it; then it runs the same check on the generator that the
secrets module uses:
import random
import secrets
import string
def undo_right(value, shift):
"""Undo value ^= value >> shift on a 32-bit number."""
result = value
for _ in range(32 // shift):
result = value ^ (result >> shift)
return result
def undo_left(value, shift, mask):
"""Undo value ^= (value << shift) & mask on a 32-bit number."""
result = value
for _ in range(32 // shift):
result = value ^ ((result << shift) & mask)
return result & 0xFFFFFFFF
def unscramble(output):
"""Turn one 32-bit output back into the state number it was made from."""
y = undo_right(output, 18)
y = undo_left(y, 15, 0xEFC60000)
y = undo_left(y, 7, 0x9D2C5680)
return undo_right(y, 11)
def predicts(generator):
"""Watch 624 outputs, copy the state they reveal, and check the copy's next five numbers."""
seen = [generator.getrandbits(32) for _ in range(624)]
copy = random.Random()
copy.setstate((3, tuple(unscramble(n) for n in seen) + (624,), None))
upcoming = [generator.getrandbits(32) for _ in range(5)]
return upcoming == [copy.getrandbits(32) for _ in range(5)], copy
server = random.Random() # seeded by the operating system: nobody knows its seed
hit, copy = predicts(server)
print("next 5 numbers predicted:", hit)
alphabet = string.ascii_letters + string.digits
token = "".join(server.choice(alphabet) for _ in range(20))
guess = "".join(copy.choice(alphabet) for _ in range(20))
print("20-character token guessed:", token == guess)
# The same check on the generator behind the secrets module: its outputs reveal nothing
hit, _ = predicts(secrets.SystemRandom())
print("secrets.SystemRandom predicted:", hit) Output
next 5 numbers predicted: True 20-character token guessed: True secrets.SystemRandom predicted: False
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 predict_next.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
After seeing 624 numbers, the copy predicts the next five exactly, and with them a 20-character “reset token” built
with choice(). The same attempt on secrets.SystemRandom fails, because its numbers come from the operating system’s
most secure source and have no state to copy. The difference in a table:
| Property | random |
secrets |
|---|---|---|
| Numbers come from | the Mersenne Twister, started from a seed | the operating system’s most secure source |
| The same seed again | gives the same numbers | there is no seed |
| 624 outputs in a row | reveal every number that follows | reveal nothing, as the last line shows |
| Made for | games, simulations and tests | passwords, tokens, one-time codes and keys |
For anything that guards an account or money (passwords, reset links, session ids, one-time codes, API keys) use the
secrets module:
import secrets
import string
# The values change on every run, so this prints only their shape.
token = secrets.token_urlsafe(32) # 32 random bytes, Base64-encoded for URLs
print(len(token), set(token) <= set(string.ascii_letters + string.digits + "-_"))
alphabet = string.ascii_letters + string.digits
password = "".join(secrets.choice(alphabet) for _ in range(16))
print(len(password), password.isalnum())
code = f"{secrets.randbelow(1_000_000):06d}" # a six-digit one-time code
print(len(code), code.isdigit())
# Compare a submitted token with the stored one in constant time
print(secrets.compare_digest(token, token), secrets.compare_digest(token, token[:-1] + "?")) Output
43 True 16 True 6 True True False
Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 secret_tokens.py
Runs on this device, in your browser. The first run downloads Python (about 13.5 MB), which is kept for the next runs.
Your run, in this browser
secrets.token_urlsafe(32)gives 32 random bytes as 43 characters that are safe in a URL. The documentation names 32 bytes, 256 bits, as enough for typical uses;token_hex()andtoken_bytes()work the same way.secrets.choice()picks a character, andsecrets.randbelow(n)a whole number from 0 to n − 1, here padded to a six-digit code.secrets.compare_digest()compares two secrets in constant time, so the time an answer takes does not reveal how many leading characters were right.
The program prints only lengths and checks because the tokens change on every run, which is exactly what they should do.
Checkpoint
Check: secrets instead of random
Practise: shared birthdays and a marks summary
How many people must be in a room before two of them probably share a birthday? Fewer than most people guess: 23 is enough for a chance just above one half. Simulate it with a generator of your own, then summarise a list of marks; the sample tests allow for the random error of a simulation.
Exercise · Easy · Python
Simulate shared birthdays and summarise marks
Write two functions in simulate.py.
simulate_birthday(n, trials, seed) estimates the chance that at least two people in a group of n share a birthday. Treat a year as 365 equally likely days and ignore leap days. Each trial gives the n people random birthdays; count the trials in which two or more birthdays are the same, and return that count divided by trials. Draw every number from random.Random(seed), so that the same arguments always give the same answer.
summarise(marks) returns a dictionary describing a list of marks, with these keys:
"count": how many marks there are"mean"and"median""stdev": the sample standard deviation"lowest"and"highest""quartiles": the three cut points ofstatistics.quantiles()with its default method
With fewer than two marks the standard deviation does not exist: let summarise() raise a ValueError then.
simulate_birthday(23, 4000, seed=1)is close to the exact answer, 0.5073simulate_birthday(1, 100, seed=1)is0.0, andsimulate_birthday(366, 10, seed=1)is1.0summarise([72, 85, 64, 91, 58, 77, 85, 69, 94, 81])["quartiles"]is[67.75, 79.0, 86.5]
The sample tests compare the estimates with the exact answers, allowing for the random error of a simulation.
Starter code · simulate.py
import random
import statistics
def simulate_birthday(n, trials, seed):
"""Estimate the chance that at least two of n people share a birthday (365 equally likely days)."""
# Replace this line with your code.
return 0.0
def summarise(marks):
"""Return the count, mean, median, sample stdev, lowest, highest and quartiles of the marks."""
# Replace this line with your code.
return {} The sample tests · test_simulate.py
import math
from simulate import simulate_birthday, summarise
def test_twenty_three_people():
"""estimates about a 50.7 % chance for 23 people"""
estimate = simulate_birthday(23, 4000, seed=1)
assert math.isclose(estimate, 0.5073, abs_tol=0.03)
def test_other_group_sizes():
"""gets close to the exact answers for 10 and 50 people"""
assert math.isclose(simulate_birthday(10, 4000, seed=4), 0.1169, abs_tol=0.03)
assert math.isclose(simulate_birthday(50, 2000, seed=3), 0.9704, abs_tol=0.03)
def test_certain_cases():
"""gives 0.0 for one person and 1.0 for 366 people"""
assert simulate_birthday(1, 100, seed=1) == 0.0
assert simulate_birthday(366, 10, seed=1) == 1.0
def test_repeatable():
"""returns the same estimate for the same seed"""
assert simulate_birthday(30, 500, seed=7) == simulate_birthday(30, 500, seed=7)
def test_summary():
"""describes ten marks"""
summary = summarise([72, 85, 64, 91, 58, 77, 85, 69, 94, 81])
assert summary["count"] == 10
assert math.isclose(summary["mean"], 77.6)
assert summary["median"] == 79.0
assert math.isclose(summary["stdev"], 11.7587, abs_tol=0.0001)
assert summary["lowest"] == 58
assert summary["highest"] == 94
assert summary["quartiles"] == [67.75, 79.0, 86.5]
def test_too_few_marks():
"""raises a ValueError for a single mark"""
try:
summarise([50])
except ValueError:
return
assert False, "expected a ValueError for one mark" A hint
Make one generator with rng = random.Random(seed) and use only it. In each trial, build a list of n numbers with rng.randrange(365); two people share a birthday exactly when the list has fewer different values than people, which len(set(days)) < n tests. For summarise(), the statistics module has mean, median, stdev and quantiles, and stdev() already raises statistics.StatisticsError, a kind of ValueError, when it gets fewer than two values.
Results of the sample tests
| Test | Result | Details |
|---|
What your code printed
The sample tests run on this device, in your browser (Pyodide): nothing is sent to mysmartcopilot.com. The first run downloads Python (about 13.5 MB), which is kept for the next runs. A check in your browser is feedback for you, not proof that the code is right for every input.
Eighteen questions on the six ideas of this lesson follow. A session opens with the idea your answers show you know least, moves up a level after each right answer, and says why every choice is right or wrong the moment you pick it:
Practice that adapts to you
Practise this lesson
18 questions about 6 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What is the difference between random.sample() and random.choices()?
sample(population, k) draws without replacement: it returns k items from k different positions, so an item that
appears once in the population appears at most once in the result, and k may not be larger than the population.
choices(population, weights=None, k=1) draws with replacement: each of the k picks is independent, so items can
repeat, k can be any size, and weights or cumulative weights make some items more likely than others. Use sample() for
a raffle, a quiz without repeated questions or splitting data into a test set; use choices() for simulations in which
the same outcome can happen again, such as customers choosing products. For a shuffled copy of a whole list, sample(x, k=len(x)) does the job.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Mid to senior
A test suite calls random.seed(1234) once at start-up, and one test compares random.shuffle() of ten user ids with an order stored in the test. Since the suite started running tests in parallel workers, that test fails now and then. Explain why, say what a Python upgrade could do to it, and rewrite the test.
-
Fresher to mid
A festival sale gives a coupon to 1 order in 50 at random, a store expects 400 orders a day, and the budget covers 14 coupons. Estimate by simulation how likely a day needs 15 or more, to within half a percentage point. How many trials do you run, and how do you know the code is right?
Key takeaways
randomproduces a repeatable stream from a seed, shared by every function of the module; give each simulation or test its ownrandom.Random(seed), and test seeded code by its properties, because onlyrandom()is promised to keep its stream across versions.choices()picks with replacement and weights,sample()without repeats, andshuffle()reorders a list in place and returnsNone;randint()includes both ends andrandrange()stops before its end.- A simulation estimates a probability as the share of trials in which the event happened, and an exact answer such as 1 − (5/6)⁴ is the best check of its code.
- A simulation’s typical error is √(p(1 − p) / n): ten times the trials shrink it about 3.2 times, and halving it costs four times the trials.
statisticsgives the mean, median, mode, sample and population standard deviations, quantiles, correlation and a regression line, and raisesStatisticsErrorwhen there are too few values.- Never use
randomfor secrets, because 624 outputs reveal its state: usesecrets.token_urlsafe(),secrets.choice(),secrets.randbelow()andsecrets.compare_digest().
Terms in this lesson
- binomial distribution
- The number of successes in n independent tries that each succeed with probability p. The chance of exactly k successes is C(n, k) × pᵏ × (1 − p)ⁿ⁻ᵏ; the sixes in four rolls of a die follow it with n = 4 and p = 1/6.
- pseudo-random
- Made by a fixed algorithm that only looks random. The random module's Mersenne Twister turns a seed into such a stream, so anyone who learns the generator's state can work out every number that follows.
- quartile
- One of the three cut points that split sorted data into four groups of the same size; the middle one is the median. statistics.quantiles(data, n=4) returns all three.
- seed
- The starting value of a pseudo-random generator. The same seed gives the same stream of numbers, so random.seed(42) or random.Random(42) makes a simulation or a test repeatable.
- standard error
- The typical distance between an estimate and the true value. For a probability p estimated from n independent trials it is √(p(1 − p) / n), so four times as many trials halve it.
- with replacement
- Drawing so that every pick can take any item again, as if each one were put back first. random.choices() draws with replacement; random.sample() draws without, using each position at most once.
References
- random, generate pseudo-random numbers (Python Software Foundation)
- random: notes on reproducibility (Python Software Foundation)
- statistics, mathematical statistics functions (Python Software Foundation)
- secrets, generate secure random numbers for managing secrets (Python Software Foundation)
- Mersenne Twister: a 623-dimensionally equidistributed uniform pseudo-random number generator (ACM Transactions on Modeling and Computer Simulation)
- NIST/SEMATECH e-Handbook of Statistical Methods: confidence intervals for a proportion (National Institute of Standards and Technology)
- NIST/SEMATECH e-Handbook of Statistical Methods: binomial distribution (National Institute of Standards and Technology)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress