Use the secrets module for anything an attacker must not guess: session tokens, password reset links, API keys and invite codes. The random module is built for simulation and modelling, and the Python docs say secrets should be preferred for security and cryptography.
What is wrong with random
The random module uses a general purpose pseudo-random number generator. It is fast and repeatable, which is what you want for shuffling a test set or running a simulation. It is not designed to resist someone who sees some outputs and tries to predict the next ones.
The docs for secrets say it should be used in preference to the default generator in random, which is "designed for modelling and simulation, not security or cryptography."
The three token helpers
The secrets module gives you three functions, each taking an optional number of bytes:
token_bytes(n)returns raw bytes.token_hex(n)returns a hex string with two characters per byte.token_urlsafe(n)returns URL safe Base64 text, with roughly 1.3 characters per byte.
import secrets
reset_token = secrets.token_urlsafe(32)
api_key = secrets.token_hex(32)
How long should a token be
The docs say that as of 2015, 32 bytes (256 bits) of randomness is believed to be sufficient for typical uses, and that this figure grows as computers get faster. They also note the default length is subject to change at any time, including in maintenance releases.
That second point is easy to miss. If you call token_urlsafe() with no argument, your token length can change when Python updates. Pass the byte count yourself so the length is a decision you made, and so database columns and validators stay consistent.
Compare secrets in constant time
When you check a submitted token against a stored one, a plain == comparison can return sooner when the first characters differ. That timing difference can leak information. The docs provide secrets.compare_digest(a, b), which uses a constant time comparison to reduce the risk of timing attacks.
import secrets
def token_matches(submitted, stored):
return secrets.compare_digest(submitted, stored)
Tokens are not passwords
The docs also warn that applications should not store passwords in a recoverable format. A random token is something you generate and hand out. A user password is something you must hash with a slow, salted algorithm. See choosing a password hashing algorithm for how to pick one.
Common mistakes to look for in review
- Seeding
randomwith the time or a user id and treating the result as secret. - Building a token from a UUID version that is not random, or from a hash of the username.
- Truncating tokens to fit a short column.
- Comparing tokens with
==. - Logging tokens at debug level.
A quick search for import random in authentication, email and billing modules finds most of these.
Safeguard's SBOM and dependency scanning helps with the neighboring problem, which is the third party packages in your tree. Try the free scan at gold.safeguard.sh/scan.
Quick check
Is random.SystemRandom acceptable?
It draws from the operating system source, but the secrets module is the documented, purpose built choice and gives you the token helpers. Prefer secrets so reviewers see intent.
How many bytes should I pass to token_urlsafe?
The docs describe 32 bytes as sufficient for typical uses as of 2015. Pass the number explicitly rather than relying on the default.
Sources
Last reviewed: 19 October 2026.