The secrets module in the Python standard library generates cryptographically secure random numbers and tokens. Unlike the random module, which is designed for simulations and games, secrets is designed for security: passwords, API keys, session tokens, password reset links, and authentication codes.
import secrets
token = secrets.token_hex(16)
print(token) # a7f3c09e1b2d4588f6e04c3179a25bf3secrets.token_hex(16) generates 16 random bytes and returns them as a 32-character hexadecimal string. The randomness comes from the operating system's secure entropy source, making it unpredictable even to attackers with significant resources.
Why secrets is different from random
The random module uses the Mersenne Twister algorithm. It is fast and produces high-quality randomness for statistics and simulations, but it is predictable: an attacker who observes enough output can determine future values. The secrets module uses the operating system's cryptographically secure random generator, which is designed to resist prediction.
import secrets
import random
print(secrets.token_hex(8)) # 1a2b3c4d5e6f7a8b
print(random.randint(0, 9999)) # 3847Use random for games, shuffling data, picking random samples for analysis, and seeding simulations. Use secrets for passwords, authentication tokens, API keys, session identifiers, and any value that an attacker must not be able to guess.
Generating secure tokens
The module provides three token formats, each suited to different contexts.
import secrets
print(secrets.token_bytes(16)) # b'\x8f\xa3\x1c\x9e\xb4\xd2\x7f\x61\x05\xe8\x3a\xc6\xf9\x2d\x4b\x0e'
print(secrets.token_hex(16)) # e4f5a6b7c8d9e0f1a2b3c4d5e6f7a8b9
print(secrets.token_urlsafe(16)) # q3R_fG7kLm2NpQ8sVwXyZ| Function | Output | Best for |
|---|---|---|
| token_bytes(n) | n random bytes | Raw binary keys |
| token_hex(n) | 2n hex characters | Database columns, logs |
| token_urlsafe(n) | ~1.3n base64 characters | URLs, query parameters |
token_hex produces only characters 0-9 and a-f, which are safe in any text context. token_urlsafe uses base64 encoding with - and _ instead of + and /, so the result is safe in URLs and filenames without percent-encoding.
For most purposes, 32 bytes (256 bits) is a safe default:
token = secrets.token_hex(32) # 64 hex charactersGenerating random passwords
Tokens are great for machine-to-machine secrets, but people still need readable passwords they can type. Combine secrets.choice() with character sets to build a password generator that picks each character independently and unpredictably.
import secrets
import string
def generate_password(length=16):
alphabet = string.ascii_letters + string.digits + "!@#$%^&*"
return "".join(secrets.choice(alphabet) for _ in range(length))
for _ in range(3):
print(generate_password())Each call produces a different 16-character password drawn independently from letters, digits, and symbols, so no two runs of the loop print the same value:
Kx9#mP2$vL7nQ@w5
Rj3^Fy8*Bc6Mp4$D
Wt5@Hq9!Nz2Xv7#Gstring.ascii_letters provides a-z and A-Z. string.digits provides 0-9. Adding a few symbols increases the character space and makes the password stronger.
For cases where you need a specific character mix, use separate sets and ensure at least one character from each:
import secrets
import string
def strong_password(length=16):
sets = [string.ascii_lowercase, string.ascii_uppercase, string.digits, "!@#$%^&*"]
password = [secrets.choice(s) for s in sets]
password += [secrets.choice("".join(sets)) for _ in range(length - 4)]
secrets.SystemRandom().shuffle(password)
return "".join(password)
print(strong_password()) # pK3%r8$x9M^nQ2!aThis guarantees at least one lowercase, one uppercase, one digit, and one symbol. After picking the required characters, it fills the rest randomly and shuffles the result so the guaranteed characters are not at predictable positions.
Safe token comparison
secrets.compare_digest() compares two strings in constant time. This prevents timing attacks where an attacker measures how long the comparison takes to guess a token character by character.
| == | secrets.compare_digest() | |
|---|---|---|
| Comparison time | Varies with match length | Constant, regardless of match length |
| Leaks timing information | Yes | No |
| Safe for secret values | No | Yes |
| Safe for public checksums | Yes | Yes, but unnecessary |
import secrets
expected = "a7f3c09e1b2d4588f6e04c3179a25bf3"
actual = "a7f3c09e1b2d4588f6e04c3179a25bf3"
if secrets.compare_digest(expected, actual):
print("Token valid")
else:
print("Token invalid")Since expected and actual hold the identical string in this example, the comparison succeeds and the branch prints the success message, even though the check itself ran in constant time regardless of the outcome:
Token validUsing == for token comparison is vulnerable to timing attacks because Python short-circuits the comparison at the first different character. secrets.compare_digest always examines the full length of both strings, so the response time reveals nothing about how many characters matched.
Practical example: generating a password reset token
A common pattern in web applications: generate a secure token for a password reset link.
import secrets
class PasswordReset:
def create_token(self):
return secrets.token_urlsafe(32)
def verify_token(self, stored_token, provided_token):
return secrets.compare_digest(stored_token, provided_token)
reset = PasswordReset()
token = reset.create_token()
print(f"Reset link: /reset-password?token={token}")
print(f"Verify valid: {reset.verify_token(token, token)}")
print(f"Verify invalid: {reset.verify_token(token, 'wrong')}")Verifying the token against itself succeeds, while verifying it against an unrelated string correctly fails, and both checks take the same amount of time no matter where the mismatch occurs:
Reset link: /reset-password?token=q3R_fG7kLm2NpQ8sVwXyZ...
Verify valid: True
Verify invalid: Falsetoken_urlsafe(32) produces a URL-safe token with 256 bits of entropy. compare_digest checks the token without leaking timing information. Even if an attacker sends thousands of requests with slightly different tokens, the response time is identical for every attempt.
Common mistakes
Using the random module for security. random.randint() and random.choice() are not cryptographically secure. An attacker who knows enough outputs can predict future values. Always use secrets for any value that protects access, authentication, or data integrity.
Using == for token comparison. Regular equality comparison leaks timing information. Always use secrets.compare_digest() when comparing tokens, API keys, or any secret value that comes from an untrusted source.
Generating tokens that are too short. A 4-byte token (8 hex characters) has only 4 billion possible values, which is brute-forceable in seconds. Use at least 16 bytes (32 hex characters), and prefer 32 bytes for long-lived tokens.
Forgetting that hex tokens are URL-safe but verbose. token_hex(32) produces a 64-character string, which is long for URLs. token_urlsafe(32) produces about 43 characters with the same security level. Use token_urlsafe when the token goes in a URL.
Rune AI
Key Insights
- Use
secrets.token_hex(n)to generate secure random hex tokens. - Use
secrets.token_urlsafe(n)to generate URL-safe base64 tokens. - Use
secrets.choice(sequence)for cryptographically secure random selection. - Use
secrets.compare_digest(a, b)to compare tokens in constant time. - Never use the
randommodule for passwords, tokens, or security-sensitive values. - 32 bytes (256 bits) of randomness is a safe default for token generation.
Frequently Asked Questions
Why use secrets instead of random for security?
How long should a token be?
What does secrets.compare_digest do?
Conclusion
The secrets module is the right tool for any security-sensitive randomness in Python. Use it for generating passwords, API tokens, session IDs, password reset links, and verification codes. Use random only for simulations, games, and non-security tasks. Always compare tokens with secrets.compare_digest to prevent timing attacks.
More in this topic
Format Dates and Times in Python
Learn how to use strftime and strptime to format dates as strings and parse date strings into Python datetime objects.
What Is the Python Standard Library?
Learn what the Python standard library is, why it matters, and how to use its built-in modules without installing anything extra.
Encode and Decode Base64 Data in Python
Learn how to use Python's base64 module to encode binary data as text and decode Base64 strings back to bytes.