Random

Python's random module generates reproducible pseudo-random numbers for everyday use — but it is explicitly not suitable for anything security-sensitive, which is what secrets is for.

The random module provides pseudo-random number generation for simulations, games, sampling, shuffling, and testing. This topic covers what pseudo-randomness actually means, how seeding gives reproducibility, the module's core functions, and the critical distinction between random (not cryptographically secure) and the secrets module (which is, and should be used instead for anything security-sensitive).

Pseudo-Randomness: What It Actually Means

random does not produce true randomness -- it uses a deterministic algorithm (CPython's default generator is the Mersenne Twister) that, given a starting internal state (a *seed*), produces a long sequence of numbers that *appear* statistically random and are suitable for simulations, games, and general-purpose sampling. Given the same seed, the exact same sequence of "random" numbers is produced every time -- this determinism is a feature, not a flaw, for the module's intended use cases.

CPython implementation detail: the specific generator algorithm (Mersenne Twister) and its exact statistical properties are implementation details of CPython; the language only guarantees the module's documented interface and behavior, not the exact bit sequence a given seed produces across all possible Python implementations.

Basic Functions

The most commonly used functions: random.random() returns a float in [0.0, 1.0); random.randint(a, b) returns an integer in [a, b] inclusive of both ends; random.uniform(a, b) returns a float in [a, b].

import random

print(random.random())        # e.g. 0.7364216...
print(random.randint(1, 6))   # simulate a die roll, 1 through 6 inclusive
print(random.uniform(1.5, 5.5))

Seeds and Reproducibility

random.seed(value) initializes the generator's internal state deterministically. Calling it with the same value guarantees the exact same subsequent sequence of outputs -- essential for reproducible tests, reproducible simulations, and debugging ("what exact sequence produced this failure?").

Both lines print the same five numbers -- the seed fully determines the sequence.

With no argument, random.seed() seeds from an unpredictable, OS-provided source (current time and/or OS randomness), which is what happens implicitly the first time the module is used if you never call seed() yourself.

import random

random.seed(42)
print([random.randint(1, 100) for _ in range(5)])

random.seed(42)
print([random.randint(1, 100) for _ in range(5)])  # identical sequence

Random Choices, Sampling, and Shuffling

random.choice(seq) picks one random element from a non-empty sequence; random.choices(seq, k=n) picks n elements *with* replacement (repeats allowed, optionally weighted); random.sample(seq, k=n) picks n *distinct* elements *without* replacement; random.shuffle(seq) shuffles a mutable sequence in place.

random.shuffle() mutates its argument in place and returns None -- a common mistake is writing deck = random.shuffle(deck), which discards the deck by assigning None to it.

import random

deck = list(range(1, 53))
random.seed(1)

print(random.choice(deck))            # one card
print(random.choices(deck, k=5))      # 5 cards, repeats possible
print(random.sample(deck, k=5))       # 5 distinct cards, no repeats

random.shuffle(deck)                  # shuffles deck in place
print(deck[:5])

Weighted Choices

random.choices() accepts a weights (or cum_weights) argument to bias selection toward certain elements -- useful for simulating loaded dice, weighted lotteries, or probability-driven simulations.

import random

outcomes = ["common", "rare", "legendary"]
weights = [80, 18, 2]   # relative weights, needn't sum to 100

random.seed(7)
drops = random.choices(outcomes, weights=weights, k=10)
print(drops)

SystemRandom: OS-Backed Randomness

random.SystemRandom is a class in the random module that draws from the operating system's own randomness source (e.g. /dev/urandom on Unix-like systems) instead of the deterministic Mersenne Twister algorithm. It exposes the same interface (.random(), .randint(), .choice(), etc.), but its output is not seedable/reproducible in the way the default generator is -- it's meant for cases needing better randomness quality than the default generator.

SystemRandom improves randomness *quality* but the random module as a whole -- including SystemRandom -- is still documented as unsuitable for security-critical purposes; for that, use the dedicated secrets module below.

import random

sys_random = random.SystemRandom()
print(sys_random.randint(1, 100))
print(sys_random.choice(["heads", "tails"]))

The secrets Module Distinction

The secrets module (standard library, Python 3.6+) is purpose-built for generating cryptographically strong random values -- tokens, passwords, API keys, session identifiers -- anything where an attacker predicting the value would be a security failure. Python's own documentation explicitly warns that the general-purpose random module's pseudo-random generator is not suitable for security/cryptographic purposes, because its internal state can in principle be inferred from enough observed outputs, letting an attacker predict future values.

Rule of thumb: if predicting the "random" value would let someone impersonate a user, guess a password-reset token, or bypass a security check, use secrets, never plain random -- regardless of how random the output looks.

import secrets

token = secrets.token_hex(16)          # a secure random hex token
url_token = secrets.token_urlsafe(16)  # URL-safe secure random token
pin = secrets.randbelow(1_000_000)     # secure random int in [0, 1_000_000)

print(token, url_token, pin)
random module                       secrets module
------------                          --------------
general-purpose, fast                security-focused
seedable, reproducible                not meant to be reproducible
NOT for tokens/passwords/keys          for tokens/passwords/keys/session IDs
Mersenne Twister (CPython detail)      OS cryptographic randomness source

Practical Usage

Typical everyday uses of random: shuffling a deck or quiz-question order, randomized simulations (Monte Carlo methods), generating test data, randomized sampling from a dataset, and games. Seed explicitly at the start of any test or simulation that needs reproducible results across runs; leave it unseeded (the default) for genuinely varied behavior each run, like a real game.

Edge Cases

random.sample(population, k) raises ValueError if k exceeds the population size, since sampling without replacement can't return more distinct items than exist.

random.choice([]) raises IndexError on an empty sequence -- always check for emptiness first if the input might be empty.

Global module-level functions (random.random(), etc.) share one hidden global Random instance; in multi-threaded code, or when you need multiple independent random streams, create separate random.Random() instances instead of relying on the shared global state.

import random

rng_a = random.Random(1)
rng_b = random.Random(2)
print(rng_a.random(), rng_b.random())  # independent streams, independently seeded

Best Practices

Seed explicitly in tests and simulations that must be reproducible; never seed (or seed from OS entropy) for real gameplay/production randomness. Use random.sample()/random.choices() instead of manual loops with random.choice() for sampling tasks -- they're both clearer and correctly avoid/allow repeats as appropriate. Never use random for anything security-sensitive; use secrets instead, every time, without exception.

Complete Example

import random
import secrets

def deal_hand(deck: list[int], hand_size: int, seed: int | None = None) -> list[int]:
    rng = random.Random(seed)   # independent, optionally-seeded stream
    return rng.sample(deck, hand_size)

def generate_reset_token() -> str:
    return secrets.token_urlsafe(32)   # security-sensitive -- use secrets, not random

deck = list(range(1, 53))
print("reproducible hand:", deal_hand(deck, 5, seed=99))
print("reproducible hand again:", deal_hand(deck, 5, seed=99))  # identical
print("password reset token:", generate_reset_token())          # unpredictable, secure

Quick Reference

random.random()              float in [0.0, 1.0)
random.randint(a, b)          int in [a, b], inclusive
random.uniform(a, b)          float in [a, b]
random.choice(seq)            one random element
random.choices(seq, k=n)      n elements, WITH replacement (repeats OK)
random.sample(seq, k=n)       n DISTINCT elements, no replacement
random.shuffle(seq)           shuffle in place, returns None
random.seed(value)            deterministic, reproducible sequence
random.Random(seed)           independent generator instance
random.SystemRandom()          OS-backed randomness, better quality, still not "secure" per docs
secrets.token_hex/urlsafe()    cryptographically secure -- use for tokens/passwords/keys

Key Takeaways

random generates deterministic pseudo-randomness (CPython: Mersenne Twister) -- given the same seed, the same sequence is always produced.

random.seed(value) makes output reproducible, essential for tests and reproducible simulations; leave unseeded for genuine per-run variation.

choices() samples with replacement (repeats allowed); sample() samples without replacement (distinct elements); shuffle() mutates in place and returns None.

SystemRandom uses OS randomness for better quality, but the random module overall is explicitly documented as unsuitable for security purposes.

For tokens, passwords, API keys, or anything security-sensitive, always use the secrets module instead of random.

deck = [1, 2, 3] deck = random.shuffle(deck) # deck becomes None deck = [1, 2, 3] random.shuffle(deck) # shuffles in place; use deck directly afterward random.shuffle() mutates its argument in place and returns None; assigning its return value overwrites the list with None.

token = str(random.random()) # used as a password-reset token import secrets token = secrets.token_urlsafe(32) random's generator is not cryptographically secure and its state can in principle be inferred from outputs; security-sensitive tokens must come from secrets.

sample = random.sample(population, k=10) # population has only 5 items k = min(10, len(population)) sample = random.sample(population, k=k) random.sample() raises ValueError when k exceeds the population size, since sampling without replacement can't return more distinct items than exist.

What does it mean for random's output to be 'pseudo-random' rather than truly random?

It's generated by a deterministic algorithm from an internal state (seed) -- the same seed always produces the same sequence of outputs, even though the outputs look statistically random; true randomness would come from an unpredictable physical source.

What does random.seed(value) do, and why is it useful in tests?

It initializes the generator's internal state deterministically so that subsequent calls to random functions produce the exact same sequence every run; tests use this to get reproducible, repeatable results instead of a different outcome on every test run.

What is the difference between random.choices() and random.sample()?

choices() samples with replacement, so the same element can be picked more than once, and needs k any size; sample() samples without replacement, returning distinct elements, and raises ValueError if k exceeds the population size.

Why does random.shuffle(my_list) not need (and should not have) its result assigned back to my_list?

shuffle() mutates the list in place and returns None; assigning the call's result back to the variable (my_list = random.shuffle(my_list)) would overwrite the shuffled list with None.

Why is the random module explicitly unsuitable for generating security tokens or passwords, even when using SystemRandom?

Because CPython's documentation states random (including its default Mersenne Twister generator) is not designed for cryptographic/security purposes -- its state can, in principle, be reconstructed from enough observed outputs. Even SystemRandom improves quality but the module as a whole isn't the documented tool for security use; secrets is.

What should you use instead of random for generating a password-reset token or API key, and why?

The secrets module -- functions like secrets.token_hex() or secrets.token_urlsafe() -- because it's specifically designed and documented for cryptographically secure random generation appropriate for security-sensitive values.

Why might you create a separate random.Random(seed) instance instead of using the module-level random.random() functions?

The module-level functions share one hidden global generator instance; creating a separate random.Random() instance gives you an independent, separately-seeded stream, useful for multi-threaded code or when different parts of a program need reproducible-but-distinct random sequences.

Write a function roll_dice(num_dice, seed=None) that returns a list of num_dice results of rolling a 6-sided die, reproducible when a seed is given.

import random def roll_dice(num_dice: int, seed: int | None = None) -> list[int]: rng = random.Random(seed) return [rng.randint(1, 6) for _ in range(num_dice)] print(roll_dice(5, seed=3)) print(roll_dice(5, seed=3)) # identical, because seed is fixed

Both calls print the same list of 5 dice results, since seed=3 fully determines the sequence produced by that Random instance.

What does this print, and why?

True. Re-seeding with the same value (0) resets the generator's internal state, so the very next randint() call produces the identical result both times.

Write a function generate_api_key() that returns a 24-character URL-safe cryptographically secure random string, using the correct module for security-sensitive randomness.

import secrets def generate_api_key() -> str: return secrets.token_urlsafe(18) # ~24 URL-safe characters print(generate_api_key())

A printed URL-safe random string of roughly 24 characters. secrets.token_urlsafe(n) generates n random bytes and base64-encodes them URL-safely, using an OS cryptographic randomness source appropriate for API keys -- unlike anything from the random module.

import random
random.seed(0)
a = random.randint(1, 10)
random.seed(0)
b = random.randint(1, 10)
print(a == b)