Call the random module twice and each run gives different numbers, then seed it once and the same numbers return like they never left.
The module is deterministic by design, which means the surprise most readers hit is not a bug. I spent this refresh seeding, shuffling, and sampling on Python 3.11.16, so you can replay sequences on demand and see exactly where the module stops being the right tool.
Random Numbers in Python Are Deterministic by Design
Behind the module sits the Mersenne Twister, a generator with a period so long you will never exhaust it. Every call derives from that single stream, so the numbers only look unpredictable until you fix the starting point.
Deterministic cuts both ways, and I confirmed each edge by running it. Reproducibility makes debugging possible, while the same determinism disqualifies the module for anything security-shaped, a boundary the docs state plainly.
| Need | Function family | Section |
|---|---|---|
| Replay the same numbers | seed, getstate, setstate | Seed and state below |
| Integers in a range | randint, randrange, getrandbits | Integers below |
| Pick from a collection | choice, choices, sample, shuffle | Sequences below |
| Floats and curves | random, uniform, gauss, triangular | Floats below |
What You Need Before You Generate Anything
Import the module and check your interpreter, since behavior below was captured on 3.11.16. One import covers everything in this guide except the security boundary, which lives in a different module entirely.
import random
import sys
print(sys.version.split()[0])
| Situation | Setup |
|---|---|
| Following this guide | import random, nothing else |
| Replaying exact outputs | Copy the seed call above each snippet |
| Generating tokens or secrets | Skip to the secrets boundary section |
Generate Exactly the Randomness You Asked For
One seeded pipeline carries this section, so every output below replays exactly if you copy the seed. Each heading answers one job, in the order you meet them.
Seed Once to Replay a Sequence
Seeding fixes the stream, so I ran the same three draws twice under one seed and both runs agreed digit for digit.
import random
random.seed(42)
first = [random.randint(1, 100) for _ in range(3)]
random.seed(42)
second = [random.randint(1, 100) for _ in range(3)]
print(first)
print(second)
print("identical:", first == second)
[82, 15, 4]
[82, 15, 4]
identical: True
Anyone with your seed replays your stream, which is another reason secrets stay out of this module. Skip the seed and each run diverges, the behavior you want for play but not for tests.
Save and Restore State Mid-Stream
Seed sets the start, while getstate and setstate bookmark any point inside, and I drew three numbers, rewound to the bookmark, and watched the same three come out again.
import random
random.seed(1)
state = random.getstate()
run1 = [random.randint(1, 1000) for _ in range(3)]
random.setstate(state)
run2 = [random.randint(1, 1000) for _ in range(3)]
print(run1)
print(run2)
print("identical:", run1 == run2)
[138, 583, 868]
[138, 583, 868]
identical: True
State objects let long simulations checkpoint without restarting from the seed. Your own Random instance isolates this further, so library code never disturbs the global stream.
import random
mine = random.Random(99)
print([mine.randint(1, 10) for _ in range(3)])
Integers With randint and randrange
randint includes both ends, while randrange stops before the end like range does. I assumed they were interchangeable once, and the off-by-one in a dice roller corrected me.
import random
random.seed(3)
print([random.randint(1, 6) for _ in range(5)])
random.seed(3)
print([random.randrange(0, 10, 2) for _ in range(5)])
[2, 5, 5, 2, 3]
[2, 8, 8, 2, 4]
The docs confirm randint is an alias for randrange with the top raised by one. Reversed bounds raise ValueError instead of guessing, which the edge section shows.
import random
random.seed(7)
print(random.getrandbits(16))
One Pick, Weighted Picks, and Unique Samples
choice takes one element, choices takes several with optional weights, and sample takes several with no repeats. Mixing these up is the most common reader error I see in questions.
import random
random.seed(11)
print([random.choice(["red", "green", "blue"]) for _ in range(3)])
random.seed(11)
print(random.choices(["red", "green", "blue"], weights=[70, 20, 10], k=10))
['green', 'blue', 'green']
['red', 'red', 'blue', 'red', 'red', 'red', 'red', 'red', 'red', 'green']
Weights tilt the draw without excluding anything, since blue still appears once above. sample instead guarantees uniqueness, and asking for more than the population holds fails loudly.
import random
random.seed(5)
print(random.sample(range(100), 5))
try:
random.sample([1, 2, 3], 5)
except ValueError as e:
print("ValueError:", e)
[79, 32, 94, 45, 88]
ValueError: Sample larger than population or is negative
Shuffle In Place or Copy
shuffle rearranges the list you hand it and returns nothing, which surprises everyone exactly once. I watched the return value come back None before believing the docs.
import random
deck = list("ABCD")
random.seed(1)
result = random.shuffle(deck)
print(deck)
print("returns:", result)
random.seed(1)
print(random.sample(list("ABCD"), k=4))
['D', 'A', 'C', 'B']
returns: None
['B', 'C', 'A', 'D']
Need the original order kept, sample with k set to the full length hands back a shuffled copy. The two runs above share a seed yet differ because shuffle and sample consume the stream differently.
Floats and Distributions
random gives a float from zero inclusive to one exclusive, and I verified the bounds across ten thousand draws in the snippet below.
import random
random.seed(13)
print([round(random.uniform(1.0, 10.0), 3) for _ in range(3)])
random.seed(13)
print([round(random.gauss(0, 1), 3) for _ in range(4)])
random.seed(17)
print([round(random.triangular(1.0, 10.0, 5.0), 3) for _ in range(3)])
[3.331, 7.167, 7.157]
[-0.086, 1.518, -0.783, -1.781]
[5.362, 7.051, 8.667]
uniform stretches the interval anywhere you like, while gauss centers draws on a mean and triangular centers them on a mode you choose.
import random
random.seed(21)
vals = [random.random() for _ in range(10000)]
print("min", round(min(vals), 6), "max", round(max(vals), 6))
print("all inside zero inclusive, one exclusive:", all(0.0 <= v < 1.0 for v in vals))
min 6.8e-05
max 0.999918
all inside zero inclusive, one exclusive: True
When the random Module Is the Wrong Tool
Determinism disqualifies this module wherever an adversary watches. Passwords, tokens, and anything security-shaped belong to the secrets module, whose output also runs fine on this machine.
import secrets
tok = secrets.token_hex(8)
print("length:", len(tok))
print("hex only:", all(c in "0123456789abcdef" for c in tok))
length: 16
hex only: True
| Mistake | What happens | Do this instead |
|---|---|---|
| randrange with start above stop | ValueError, empty range | Order the bounds low to high |
| sample bigger than the population | ValueError | Use choices when repeats are fine |
| Expecting shuffle to return the list | None, list changed in place | Use the list after the call, or sample a copy |
| Tokens from random | Replayable by anyone with the seed | Use secrets |
import random
try:
random.randrange(10, 1)
except ValueError as e:
print("ValueError:", e)
ValueError: empty range for randrange() (10, 1, -9)
Which Function to Reach For
One question picks the function, namely whether repeats are allowed and whether the draw must replay. Answer that and the table below finishes the job.
| Job | Function |
|---|---|
| Replay a whole stream | seed, or getstate and setstate mid-stream |
| Integer, both ends included | randint |
| Integer from a range with steps | randrange |
| Single element | choice |
| Several elements, repeats fine, weights wanted | choices |
| Several unique elements | sample |
| Reorder a list | shuffle, or sample for a copy |
| Float in an interval | uniform |
| Anything an adversary must not predict | secrets, not random |
Frequently Asked Questions
Direct answers to the random questions readers keep asking. Each one points back at the section that proves it.
How do I get the same random numbers every run in Python?
Call random.seed with a fixed value before drawing, and the stream replays identically. Use getstate and setstate to bookmark a point mid-stream instead of restarting from the seed.
What is the difference between randint and randrange?
randint includes both ends, so randint 1, 6 can return 6. randrange stops before the end like range does, and it also accepts a step. randint is an alias for randrange with the top raised by one.
Should I use choice, choices, or sample?
choice draws one element. choices draws several with replacement and accepts weights. sample draws several unique elements and raises ValueError when you ask for more than the population holds.
Why does shuffle return None?
shuffle rearranges the list in place and returns nothing by design. Read the list after the call, or use sample with k set to the full length when you need a shuffled copy.
Can I use random for passwords or tokens?
No. The module is deterministic, so anyone with your seed replays your stream. Use the secrets module for passwords, tokens, and anything security-shaped.

