random.randint(a, b) returns a random integer N where a <= N <= b, and the part that catches everyone is that both ends are included. randint(1, 6) can return 6, unlike almost every other range-like call in Python where the upper bound is exclusive. That single inconsistency is the source of most randint bugs. The other, quieter problem is that random.randint is not safe for anything security-related, and the language will not warn you when you use it for a password reset token.
It is a two-argument function, so how much is there to say? Enough that the inclusive upper bound and the security caveat are worth pinning down before you ship the code that uses them.
Table of contents
- The inclusive range is the whole gotcha
- randint vs randrange vs choice
- Seeding for reproducibility
- The security trap: never use randint for tokens
- Generating a list of random integers
- How this fits the rest of the stack
- FAQ
The inclusive range is the whole gotcha
Compare it to the calls it sits next to:
import random
random.randint(1, 6) # 1, 2, 3, 4, 5, or 6 - both ends included
random.randrange(1, 6) # 1, 2, 3, 4, or 5 - stop is EXCLUDED, like range()
range(1, 6) # 1, 2, 3, 4, 5 - stop excluded
So randint(0, len(items)) can return len(items), which is one past the last valid index and an IndexError waiting to happen. If you want a random valid index, you want randint(0, len(items) - 1) or, more clearly, random.randrange(len(items)). When in doubt, reach for randrange; it matches the exclusive-upper-bound convention the rest of your code already uses.
randint vs randrange vs choice
randint(a, b)- one integer, both ends inclusive. Good when the range is naturally inclusive, like a dice roll or a day of the month.randrange(start, stop, step)- likerange, stop excluded, supports a step.randrange(0, 100, 2)gives a random even number under 100.random.choice(seq)- a random element from a sequence. If you are indexing into a list to pick an element, usechoicedirectly instead of generating an index.
The rule of thumb: if you are about to write items[random.randint(0, len(items) - 1)], replace the whole thing with random.choice(items). It says what you mean and removes the off-by-one surface area.
Seeding for reproducibility
random.seed(n) makes the sequence deterministic, which is what you want in tests and simulations and never in production randomness.
import random
random.seed(42)
print(random.randint(1, 100)) # same value every run with seed 42
For tests, seed at the start so a failure is reproducible. For a Monte Carlo simulation, seed so a colleague can reproduce your run. Do not seed a web request handler with a fixed value and expect uniqueness across requests, which is a mistake that produces the same ‘random’ token for every user.
The security trap: never use randint for tokens
random.randint and everything in the random module use the Mersenne Twister, a fast pseudo-random generator that is not cryptographically secure. Given enough outputs, its internal state can be reconstructed and future values predicted. That is fine for a dice game and a disaster for a password reset code, a session ID, or an API key.
import secrets
secrets.randbelow(1000000) # a secure integer 0..999999
secrets.token_urlsafe(32) # a secure URL-safe token
secrets.choice(candidates) # a secure pick from a sequence
The secrets module is the correct tool for anything a user or attacker could benefit from predicting. It has the same shape as random for the calls you need, so switching is cheap. The only reason people reach for randint here is habit, and habit is not a threat model.
Generating a list of random integers
import random
# Ten dice rolls
rolls = [random.randint(1, 6) for _ in range(10)]
# A sample WITHOUT repeats - use sample, not a loop
lottery = random.sample(range(1, 50), 6) # 6 distinct numbers
If you need distinct values, random.sample guarantees no repeats in one call. Building a list by looping randint and checking for duplicates is slower and easy to get subtly wrong when the range is small relative to the count.
How this fits the rest of the stack
Randomness is one of those areas where the code looks finished long before it is correct: the wrong generator produces plausible output right up until someone predicts it. The same ‘looks fine, is not’ gap shows up in infrastructure sizing, where a service runs happily on undersized compute until real traffic finds the edge. The RunxBuild hosting calculator makes the compute, memory, and bandwidth for a Python service explicit line items so the total is checked rather than assumed, and the RunxBuild dashboard is where you watch the real numbers once it is live.
Useful related references:
- Generate a Random Number in Python
- Python floor(): floor, int, and // disagree on negatives
- Python services on RunxBuild
FAQ
Is randint inclusive of both numbers in Python?
Yes. random.randint(a, b) can return a, b, and every integer between them. Both ends are inclusive, which is different from range() and random.randrange(), where the upper bound is excluded. That is why randint(0, len(items)) can return an out-of-range index; use randint(0, len(items) - 1) or random.randrange(len(items)) for a valid index.
What is the difference between randint and randrange?
randint(a, b) includes both endpoints and takes no step. randrange(start, stop, step) excludes the stop value and supports a step, matching the behavior of range(). Use randrange when you want the familiar exclusive-upper-bound semantics, and randint only when an inclusive range is genuinely what you mean, like a dice roll.
Is random.randint secure for passwords or tokens?
No. The random module uses the Mersenne Twister, which is not cryptographically secure and whose future output can be predicted from enough observed values. For anything security-sensitive - reset codes, session IDs, API keys - use the secrets module: secrets.randbelow, secrets.token_urlsafe, or secrets.choice. They have the same shape and are safe to expose.
How do I generate the same random numbers every time?
Call random.seed(n) with a fixed integer before generating. The sequence becomes deterministic, which is useful in tests and simulations so a run can be reproduced. Never fix the seed in production code that needs unique values, such as a request handler, or every call produces the same result.
How do I get several distinct random integers?
Use random.sample(population, k). random.sample(range(1, 50), 6) returns six distinct numbers with no repeats in a single call. Looping randint and rejecting duplicates works but is slower and error-prone when the range is small relative to how many values you need.