Secure Passwords with Python `secrets`

Learn how to use Python's secrets module to generate cryptographically secure random tokens, passwords, and URL-safe identifiers.

6 min read

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.

pythonpython
import secrets
 
token = secrets.token_hex(16)
print(token)  # a7f3c09e1b2d4588f6e04c3179a25bf3

secrets.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.

pythonpython
import secrets
import random
 
print(secrets.token_hex(8))         # 1a2b3c4d5e6f7a8b
print(random.randint(0, 9999))  # 3847

Use 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.

pythonpython
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
FunctionOutputBest for
token_bytes(n)n random bytesRaw binary keys
token_hex(n)2n hex charactersDatabase columns, logs
token_urlsafe(n)~1.3n base64 charactersURLs, 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:

pythonpython
token = secrets.token_hex(32)  # 64 hex characters

Generating 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.

pythonpython
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:

texttext
Kx9#mP2$vL7nQ@w5
Rj3^Fy8*Bc6Mp4$D
Wt5@Hq9!Nz2Xv7#G

string.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:

pythonpython
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!a

This 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 timeVaries with match lengthConstant, regardless of match length
Leaks timing informationYesNo
Safe for secret valuesNoYes
Safe for public checksumsYesYes, but unnecessary
pythonpython
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:

texttext
Token valid

Using == 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.

pythonpython
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:

texttext
Reset link: /reset-password?token=q3R_fG7kLm2NpQ8sVwXyZ...
Verify valid:   True
Verify invalid: False

token_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

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 random module for passwords, tokens, or security-sensitive values.
  • 32 bytes (256 bits) of randomness is a safe default for token generation.
RunePowered by Rune AI

Frequently Asked Questions

Why use secrets instead of random for security?

The `random` module uses the Mersenne Twister algorithm, which is predictable if an attacker observes enough output. The `secrets` module uses the operating system's cryptographically secure random number generator, which is designed to be unpredictable even with extensive observation.

How long should a token be?

Use at least 32 bytes (256 bits) of randomness for tokens. `secrets.token_hex(32)` produces a 64-character hex string with 256 bits of entropy, which is secure against brute-force attacks for the foreseeable future.

What does secrets.compare_digest do?

`secrets.compare_digest(a, b)` compares two strings in constant time, meaning the comparison takes the same amount of time regardless of how many characters match. This prevents timing attacks where an attacker measures response time to guess a token character by character.

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.