To generate a random number in Python, use the random module: random.randint(1, 6) gives an integer from 1 to 6 inclusive, and random.random() gives a float between 0 and 1. That covers dice, shuffles, sampling, and simulations. But there is a catch worth knowing up front: random is not cryptographically secure. For passwords, tokens, API keys, or anything an attacker should not predict, use the secrets module instead. Getting that one distinction right is the difference between a toy and a vulnerability.
Table of contents
- The everyday functions
- randint, randrange, and the off-by-one
- Reproducibility with seed
- Use secrets for anything security-sensitive
- Random choices with weights
- How this fits the rest of the stack
- FAQ
The everyday functions
Import the module and reach for the right function:
import random
random.randint(1, 6) # int from 1 to 6, both ends included
random.random() # float in [0.0, 1.0)
random.uniform(1, 10) # float in [1, 10]
random.choice(["a","b","c"]) # one random element
random.sample(range(50), 6) # 6 unique elements, like a lottery
random.shuffle(my_list) # shuffle a list in place
The one that catches people is randint: it is inclusive on both ends, so randint(1, 6) really can return 6. That is different from range(1, 6), which stops at 5. If you want the half-open behaviour, random.randrange(1, 6) matches range and excludes the top.
choice picks one item, sample picks several without repeats, and shuffle reorders in place and returns None - do not write x = random.shuffle(x), or you wipe out your list, the same trap as list.sort().
randint, randrange, and the off-by-one
Three functions produce random integers and they differ at the boundaries:
random.randint(1, 10) # 1..10 inclusive
random.randrange(1, 10) # 1..9 (excludes 10, like range)
random.randrange(0, 100, 5)# 0,5,10,...,95 (a step)
randint(a, b) includes both a and b. randrange(start, stop) excludes stop, matching Python’s usual range semantics, and it accepts a step so you can draw from 0, 5, 10, and so on.
The practical advice: use randint when you are thinking in terms of dice and both-ends-inclusive ranges, and randrange when you are thinking in terms of indexes and range-style bounds. The off-by-one bugs come from expecting one and calling the other - a dice roll that occasionally never hits the maximum, or an index that reaches one past the end.
Reproducibility with seed
Sometimes you want the same random sequence every run - for tests, for reproducible experiments, for a debuggable simulation. Seed the generator:
import random
random.seed(42)
random.randint(1, 100) # same value every run with this seed
With a fixed seed, the sequence of random numbers is deterministic. This is invaluable for testing code that uses randomness - you can assert on exact outputs - and for scientific work where someone needs to reproduce your results.
The flip side is the warning label: a seeded, predictable generator is the opposite of what you want for anything security-sensitive. If your token generator is reproducible, it is guessable. Seeding is a feature for tests and simulations and a liability for secrets, which is the whole reason the next section exists.
Use secrets for anything security-sensitive
The random module uses a Mersenne Twister - excellent statistical properties, completely unsuitable for security. Its output is predictable: observe enough values and you can reconstruct the internal state and predict the rest. For passwords, session tokens, API keys, password-reset codes, or anything an attacker benefits from guessing, use secrets:
import secrets
secrets.token_hex(16) # a 32-char hex token
secrets.token_urlsafe(16) # a URL-safe token
secrets.randbelow(100) # a secure int in [0, 100)
secrets.choice(alphabet) # a secure random choice
secrets draws from the operating system’s cryptographically secure random source, which is designed to be unpredictable. The API mirrors random closely enough that switching is easy.
The rule is blunt and worth memorizing: random for games, simulations, sampling, and shuffling; secrets for anything that protects something. Using random to generate a password is a real, exploitable bug, not a style nitpick.
Random choices with weights
When outcomes are not equally likely - a weighted die, a feature flag that fires 10% of the time - use random.choices (plural) with weights:
random.choices(
["common", "rare", "legendary"],
weights=[70, 25, 5],
k=10,
) # 10 draws following the weights
choices returns a list of k picks, with replacement, respecting the weights. Note the plural: choice (singular) picks one item uniformly, while choices (plural) picks k items and supports weights. The names are one letter apart and do different things, which is a classic source of confusion.
For sampling without replacement - drawing unique items - use random.sample instead, since choices can repeat. Pick choices for weighted, with-replacement draws and sample for unique draws, and you cover almost every real sampling need.
How this fits the rest of the stack
The random-versus-secrets distinction is a small decision with a large blast radius - a predictable token generator is the kind of thing that only becomes a headline after it ships. When code that mints tokens runs as a real service, the runtime it executes in and the way you handle its secrets are part of the same security story. The RunxBuild hosting calculator lays out the service, database, storage, and bandwidth as separate line items, and the RunxBuild dashboard is where the team watches deploys, logs, and restarts as they happen.
Useful related references:
- Null in Python: There Is No null, There Is None - and That Distinction Matters
- Python Secrets: How to Stop Hardcoding Them, Where to Put Them, and the Pattern That Scales
- API vs Web Service: The Difference That Matters When You Deploy One
- Python services on RunxBuild
FAQ
How do I generate a random number in Python?
Use the random module: random.randint(1, 6) for an integer from 1 to 6 inclusive, random.random() for a float between 0 and 1, or random.uniform(a, b) for a float in a range. For security-sensitive values, use the secrets module instead.
What is the difference between randint and randrange?
random.randint(a, b) includes both endpoints, so randint(1, 10) can return 10. random.randrange(start, stop) excludes the stop value, matching Python’s range, and accepts a step. Mixing them up causes off-by-one bugs at the boundaries.
Why should I use secrets instead of random for passwords?
Because random uses a Mersenne Twister that is statistically strong but predictable - an attacker who sees enough output can predict the rest. The secrets module draws from the OS cryptographically secure source, so tokens and passwords cannot be guessed. Using random for secrets is an exploitable bug.
How do I get the same random numbers every run?
Call random.seed(42) (any fixed value) before drawing. A fixed seed makes the sequence deterministic, which is useful for tests and reproducible simulations. Never do this for security-sensitive values - a reproducible generator is a guessable one.
What is the difference between random.choice and random.choices?
random.choice (singular) returns one element chosen uniformly. random.choices (plural) returns a list of k elements with replacement and supports a weights argument for non-uniform draws. For unique draws without replacement, use random.sample.