The random module is Python's workhorse for generating random numbers, shuffling sequences, and picking samples. Under the hood it uses a Mersenne Twister, which is fast, statistically solid for simulations and games, but not suitable for anything security-related. For tokens, passwords, and session IDs use the secrets module instead.
The most common functions are random() (uniform float in [0, 1)), uniform(a, b) (uniform float in [a, b]), randint(a, b) (uniform int in [a, b] inclusive), choice(seq) (one random element), choices(seq, k=N) (with replacement), sample(seq, k) (without replacement), and shuffle(lst) (in-place permutation). Together they cover 90% of needs.
Reproducibility matters when you want to debug, test, or show an example that behaves the same for everyone. random.seed(42) seeds the generator; subsequent calls produce the same sequence. For isolated code paths that shouldn't interfere with the module-level generator, create a random.Random(seed) instance and call methods on it.
For numerical simulations with millions of draws, numpy.random is dramatically faster thanks to vectorized operations. The API mirrors random closely and is the standard in scientific Python. But random is always available and perfectly fine for the day-to-day cases this lesson covers.
Choosing the right function
Use choice for a single pick, choices when you need duplicates (with optional weights), sample when you want distinct items. shuffle is in place; use random.sample(lst, len(lst)) for a non-mutating shuffled copy.
For Gaussian noise, gauss(mu, sigma) is fast; for exponential backoff delays, random.expovariate(rate). Reach for specialized functions only when the uniform form isn't what you want.
Reproducibility and security
random.seed(n) seeds the shared generator. Everything downstream is deterministic until the next reseed. For tests, prefer random.Random(seed) so you don't affect other tests accidentally.
Never use random for passwords, tokens, cryptographic keys, or security decisions. Use secrets.token_hex, secrets.token_urlsafe, secrets.choice, secrets.randbelow. They use the OS's secure RNG and are designed for this purpose.
The random-module toolkit.
| Tool | Purpose |
|---|---|
random.random()function | Uniform float in [0.0, 1.0). |
random.randint(a, b)function | Uniform int in [a, b] inclusive. |
random.choice(seq)function | Random element from a non-empty sequence. |
random.sample(pop, k)function | k distinct elements, without replacement. |
random.choices(seq, k=n)function | k elements with replacement, optional weights. |
random.shuffle(list)function | In-place permutation. |
random.seed(n)function | Seeds the generator for reproducibility. |
secrets.token_hex / token_urlsafefunction | Cryptographically strong random tokens. |
Creating Random Values code example
The script below uses every common function against a reproducible seed so the output is stable for teaching.
# Lesson: Creating Random Values
import random
import secrets
random.seed(7) # deterministic for demo
print("random(): ", round(random.random(), 3))
print("uniform: ", round(random.uniform(10, 20), 3))
print("randint 1-6: ", random.randint(1, 6))
fruits = ["apple", "banana", "cherry", "date"]
print("choice: ", random.choice(fruits))
print("choices (x5):", random.choices(fruits, k=5))
weighted = random.choices(
["hit", "miss"], weights=[3, 1], k=10
)
print("weighted: ", weighted)
print("sample (x3): ", random.sample(fruits, k=3))
cards = list(range(1, 11))
random.shuffle(cards)
print("shuffled: ", cards)
# Gaussian (normal) distribution
print("gauss: ", round(random.gauss(mu=0, sigma=1), 3))
# Security-sensitive token: use secrets, not random
print("secure token:", secrets.token_hex(8))
# Isolated generator for tests
rng = random.Random(42)
print("isolated: ", rng.randint(1, 100), rng.randint(1, 100))
Key ideas in the script:
1) `random.seed(7)` makes all module-level calls below deterministic.
2) `choices` supports duplicates and weights; `sample` enforces uniqueness.
3) `gauss(mu, sigma)` is the common shortcut for Gaussian noise.
4) `secrets.token_hex(8)` is the right primitive for security-sensitive randomness.
Practice with a reproducible deck and a Monte-Carlo estimate.
import random
random.seed(0)
# Example A: draw 5 unique cards
deck = [f"{r}{s}" for s in "cdhs" for r in range(2, 15)]
print(random.sample(deck, 5))
# Example B: estimate pi by Monte Carlo
n_inside = 0
n = 20_000
for _ in range(n):
x, y = random.random(), random.random()
if x*x + y*y < 1:
n_inside += 1
print(f"pi approx: {4 * n_inside / n:.3f}")
Deterministic checks when the seed is fixed.
import random
random.seed(1)
a = random.randint(1, 10)
random.seed(1)
b = random.randint(1, 10)
assert a == b
assert 1 <= a <= 10
Running the script prints (seed makes values stable):
random(): 0.322
uniform: 17.156
randint 1-6: 5
choice: banana
choices (x5): ['banana', 'apple', 'cherry', 'apple', 'banana']
weighted: ['hit', 'hit', 'hit', 'miss', 'hit', 'miss', 'hit', 'hit', 'miss', 'hit']
sample (x3): ['date', 'cherry', 'apple']
shuffled: [7, 3, 2, 9, 4, 8, 10, 6, 5, 1]
gauss: 0.423
secure token: a1b2c3d4e5f60718
isolated: 82 15