The secrets module generates cryptographically secure random numbers. Use it for tokens, passwords, and security-sensitive randomness.

Why Not random?

import random
import secrets
 
# random is NOT secure - predictable seed
random.seed(42)
print(random.randint(0, 100))  # Always 81
 
# secrets uses OS entropy - unpredictable
print(secrets.randbelow(100))  # Different each time
 
# Never use random for:
# - Passwords
# - Tokens
# - Session IDs
# - API keys
# - Anything security-related

Basic Functions

import secrets
 
# Random integer below n
secrets.randbelow(100)  # 0 to 99
 
# Random bits
secrets.randbits(128)  # 128-bit random integer
 
# Random bytes
secrets.token_bytes(32)  # 32 random bytes
 
# Random hex string
secrets.token_hex(32)  # 64-character hex string
 
# URL-safe token
secrets.token_urlsafe(32)  # ~43 characters, base64

Generating Tokens

import secrets
 
# Session tokens
session_id = secrets.token_hex(32)  # 64 hex chars
# e.g., "a1b2c3d4e5f6..."
 
# URL-safe tokens (for password reset links, etc.)
reset_token = secrets.token_urlsafe(32)
# e.g., "Drmhze6EPcv0fN_81Bj..."
 
# API keys
api_key = secrets.token_urlsafe(32)
 
# Shorter tokens (when collision risk is acceptable)
short_token = secrets.token_hex(8)  # 16 hex chars

Password Generation

import secrets
import string
 
def generate_password(length=16):
    """Generate a secure random password."""
    alphabet = string.ascii_letters + string.digits + string.punctuation
    return "".join(secrets.choice(alphabet) for _ in range(length))
 
def generate_passphrase(words=4, wordlist=None):
    """Generate a passphrase from random words."""
    if wordlist is None:
        # Simple word list (use a real one in production)
        wordlist = ["apple", "banana", "cherry", "dragon", "eagle", 
                   "falcon", "grape", "hammer", "island", "jungle"]
    return "-".join(secrets.choice(wordlist) for _ in range(words))
 
# Usage
print(generate_password())      # "Kx#9mP!qR2$vL@nT"
print(generate_passphrase())    # "falcon-grape-island-dragon"

Password Requirements

import secrets
import string
 
def generate_strong_password(length=16):
    """Generate password meeting common requirements."""
    # Ensure at least one of each required type
    password = [
        secrets.choice(string.ascii_lowercase),
        secrets.choice(string.ascii_uppercase),
        secrets.choice(string.digits),
        secrets.choice(string.punctuation),
    ]
    
    # Fill the rest randomly
    alphabet = string.ascii_letters + string.digits + string.punctuation
    password += [secrets.choice(alphabet) for _ in range(length - 4)]
    
    # Shuffle to avoid predictable positions
    secrets.SystemRandom().shuffle(password)
    
    return "".join(password)

Comparing Secrets Securely

import secrets
 
# DON'T use == for secret comparison (timing attack vulnerable)
# if user_token == stored_token:  # BAD
 
# DO use constant-time comparison
if secrets.compare_digest(user_token, stored_token):
    print("Token valid")
 
# Works with strings or bytes
secrets.compare_digest(b"secret1", b"secret1")  # True
secrets.compare_digest("secret1", "secret2")    # False

Choosing Random Items

import secrets
 
# Random choice from sequence
colors = ["red", "green", "blue", "yellow"]
chosen = secrets.choice(colors)
 
# Random sample (without replacement)
# Use secrets.SystemRandom() for this
rng = secrets.SystemRandom()
sample = rng.sample(colors, k=2)
 
# Shuffle securely
items = [1, 2, 3, 4, 5]
rng.shuffle(items)

SystemRandom

import secrets
 
# Full random.Random interface with secure randomness
rng = secrets.SystemRandom()
 
# All random module methods, but secure
rng.random()           # Float [0.0, 1.0)
rng.randint(1, 100)    # Integer [1, 100]
rng.uniform(0, 10)     # Float [0, 10]
rng.choice([1, 2, 3])  # Random element
rng.shuffle([1, 2, 3]) # In-place shuffle
rng.sample([1,2,3], 2) # Random sample

Common Patterns

import secrets
 
def generate_otp(length=6):
    """Generate one-time password (numeric)."""
    return "".join(str(secrets.randbelow(10)) for _ in range(length))
 
def generate_confirmation_code():
    """Generate human-readable confirmation code."""
    # Uppercase letters + digits, excluding confusing chars
    alphabet = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"
    return "".join(secrets.choice(alphabet) for _ in range(8))
 
def generate_api_key():
    """Generate API key with prefix."""
    return f"sk_live_{secrets.token_urlsafe(32)}"
 
def generate_invite_code():
    """Generate short invite code."""
    return secrets.token_urlsafe(8)[:12]

Token Length Guidelines

import secrets
 
# Minimum recommended lengths:
 
# Session IDs: 128 bits (32 hex chars)
session = secrets.token_hex(16)
 
# CSRF tokens: 128 bits
csrf = secrets.token_hex(16)
 
# Password reset tokens: 256 bits (for longer validity)
reset = secrets.token_urlsafe(32)
 
# API keys: 256 bits
api_key = secrets.token_urlsafe(32)
 
# Short-lived OTP: 6-8 digits is common
otp = generate_otp(6)

Entropy Calculation

import secrets
import math
 
def entropy_bits(length, alphabet_size):
    """Calculate entropy in bits."""
    return length * math.log2(alphabet_size)
 
# Examples
print(entropy_bits(16, 62))   # alphanumeric: ~95 bits
print(entropy_bits(16, 94))   # ASCII printable: ~105 bits
print(entropy_bits(32, 16))   # hex: 128 bits
print(entropy_bits(32, 64))   # base64: 192 bits
 
# Recommended: at least 128 bits for security tokens

File-Based Secrets

import secrets
from pathlib import Path
 
def get_or_create_secret(path: str, length: int = 32) -> str:
    """Get existing secret or create new one."""
    secret_path = Path(path)
    
    if secret_path.exists():
        return secret_path.read_text().strip()
    
    secret = secrets.token_urlsafe(length)
    secret_path.write_text(secret)
    secret_path.chmod(0o600)  # Owner read/write only
    return secret
 
# Usage
app_secret = get_or_create_secret(".app_secret")

Best Practices

import secrets
 
# Always use secrets for security-sensitive randomness
token = secrets.token_urlsafe(32)  # Good
 
# Never use random for security
# token = ''.join(random.choices(...))  # Bad
 
# Use sufficient length (128+ bits)
secrets.token_bytes(16)  # Minimum 16 bytes
 
# Use constant-time comparison
secrets.compare_digest(a, b)
 
# Don't log or expose secrets
# print(f"Token: {token}")  # Bad for production
 
# Rotate secrets periodically
# Store securely (env vars, secret managers)

secrets vs os.urandom

import secrets
import os
 
# Both use system entropy
secrets.token_bytes(32)  # Higher-level API
os.urandom(32)           # Lower-level, same source
 
# secrets provides convenience functions
secrets.token_hex(32)     # No equivalent in os
secrets.token_urlsafe(32) # No equivalent in os
secrets.choice(items)     # No equivalent in os
secrets.compare_digest()  # Available in hmac too
 
# Use secrets unless you need raw bytes

The secrets module is your go-to for security-sensitive randomness. Never use random for passwords, tokens, or cryptographic purposes.

React to this post: