Q1Which module should you reach for to generate a password reset token?
random and secrets — Choosing the Right Random Numbers
Learn Python's random and secrets modules from the ground up. Covers reproducible pseudorandom sequences with random.choice / randint / shuffle / sample plus seed, cryptographically safe tokens with secrets.token_hex / token_urlsafe, and when to use one vs. the other — with runnable practice exercises.
This article covers the two random-number modules in the standard library. random provides pseudorandom values for tests, games, and simulations (reproducible with seed), and secrets provides unpredictable values for passwords, reset tokens, and API keys (drawn from the OS's cryptographic random source). Picking the right one for the job is what the rest of the article is about.
How random and secrets Differ
Both modules return random numbers, but the mechanism and the use cases are completely different. random is built on the Mersenne Twister (a widely used pseudorandom-number generator), and calling random.seed(value) lets you reproduce the same sequence. secrets, on the other hand, draws from the OS's cryptographic random source (such as /dev/urandom), so there's no `seed` concept and the values are unpredictable every time.
For cases like tests or games — "it's fine if the result leaks" — reach for random. For passwords, tokens, and session IDs — "someone predicting the value would be a security incident" — reach for secrets.
| Aspect | random | secrets |
|---|---|---|
| Source | Mersenne Twister (algorithm) | OS cryptographic source (/dev/urandom, etc.) |
| Reproducibility | Same sequence reproducible with seed | Not reproducible (always different) |
| Speed | Fast | Slower (not a concern for the typical use case) |
| Best fit | Tests, games, simulations | Tokens, passwords, session IDs |
random — The Basics (randint / uniform / choice)
The basics of random are three things: a random integer / a random float / picking one element from a list. random.randint(low, high) returns an integer including both ends, random.uniform(low, high) returns a float in the given range, and random.choice(list) returns one element from the list.
Let's start without seed and see how these basic functions behave.
| Function | Return | Typical use |
|---|---|---|
| random.randint(a, b) | Integer in [a, b] (both ends included) | Dice rolls, test IDs |
| random.uniform(a, b) | Float in [a, b] | Noise, probabilistic simulations |
| random.choice(seq) | One element of the sequence | Pick one item from a menu |
random — Seeded Reproducibility and Collection Operations
Now let's look at test reproducibility with `random.seed` and collection-operation functions (shuffle / sample). After calling random.seed(N), the same seed always produces the same sequence. random.shuffle(list) shuffles the list's elements in place, and random.sample(list, k) returns a new list of k unique elements drawn from it.
| Function | Return | Typical use |
|---|---|---|
| random.seed(value) | None (initializes internal state) | Reproducible tests |
| random.shuffle(seq) | None (shuffles the original list) | Randomize the order of a list |
| random.sample(seq, k) | New list of k unique elements | Pick k respondents from a survey |
secrets — Strong Random for Security
The random module from the previous sections is a pseudorandom generator for tests — if its internal state leaks, the next values can be predicted. For things like password reset tokens, API keys, and session IDs — "if an attacker can predict the value, you have a vulnerability" — pick the `secrets` module, which uses the OS's cryptographic random source.
secrets has a much smaller API than random — it's mostly helpers that turn random bytes into hex strings or URL-safe strings.
| Function | Return | Typical use |
|---|---|---|
| secrets.token_bytes(n) | n random bytes | Crypto keys, internal binary tokens |
| secrets.token_hex(n) | Hex string of length 2*n | Login session IDs |
| secrets.token_urlsafe(n) | URL-safe string (Base64-ish) | Embed in reset-link URLs |
| secrets.choice(seq) | One element from seq, cryptographically | When the display order shouldn't be predictable |
| secrets.compare_digest(a, b) | True / False (timing-attack resistant) | Hash / token comparison (use instead of ==) |
Knowledge Check
Answer each question one by one.
Q2After calling random.seed(42) and then random.randint(1, 100), what happens if you do the same thing again?
Q3Which is the best option when you want a randomly shuffled list to be reproducible for tests?