Intermediate

You have probably written code that generates random tokens for password resets, API keys, or session identifiers. If you used the random module for this, your tokens are predictable — an attacker who knows the seed can reproduce every token you generate. This is not a theoretical risk; it has been the root cause of real security breaches.

Python 3.6 introduced the secrets module specifically for generating cryptographically secure random values. It is part of the standard library, so there is nothing to install. It uses the operating system’s best source of randomness (/dev/urandom on Linux, CryptGenRandom on Windows) and produces tokens that are safe for security-sensitive applications.

In this tutorial, you will learn how to generate secure tokens in multiple formats (hex, URL-safe, bytes), create safe passwords, build one-time password reset links, and compare tokens securely. By the end, you will have a complete toolkit for handling secrets in any Python application.

Generating a Secure Token: Quick Example

Here is the fastest way to generate a cryptographically secure token in Python:

# quick_token.py
import secrets

# Generate a 32-byte URL-safe token
token = secrets.token_urlsafe(32)
print(f"Token: {token}")
print(f"Length: {len(token)} characters")

Output:

Token: x7Kj2mN9pQrS1tUvWxYz3aB4cD5eF6gH7iJ8kL0mNo
Length: 43 characters

The secrets.token_urlsafe() function generates random bytes and encodes them as a URL-safe base64 string. The 32-byte input produces a 43-character token with enough entropy (256 bits) to resist brute-force attacks. We will explore all the token types and their use cases in the sections below.

What is the Secrets Module and Why Use It?

The secrets module provides functions for generating cryptographically strong random numbers suitable for managing secrets such as account authentication, tokens, and similar. Think of it as the security-focused counterpart to random.

The key difference is the source of randomness. The random module uses a Mersenne Twister algorithm — fast and statistically uniform, but deterministic. If someone discovers the internal state (which requires observing only 624 consecutive outputs), they can predict all future values. The secrets module draws from the OS entropy pool, which is non-deterministic and cannot be reversed.

Featurerandomsecrets
AlgorithmMersenne Twister (PRNG)OS entropy pool (CSPRNG)
Predictable?Yes, if seed is knownNo
SpeedVery fastSlightly slower
Use caseSimulations, games, testingPasswords, tokens, API keys
Thread-safe?No (shared state)Yes

The rule is simple: if the value protects something, use secrets. If it does not, random is fine.

Generating Tokens in Different Formats

The secrets module offers three token functions, each producing a different encoding of the same underlying random bytes. The format you choose depends on where you plan to use the token.

Hex Tokens with token_hex()

Hex tokens produce a string of hexadecimal characters (0-9, a-f). They are commonly used for database identifiers, session IDs, and anywhere you need a clean alphanumeric string.

# hex_tokens.py
import secrets

# Generate hex tokens of different lengths
token_16 = secrets.token_hex(16)  # 16 bytes = 32 hex chars
token_32 = secrets.token_hex(32)  # 32 bytes = 64 hex chars

print(f"16-byte hex: {token_16}")
print(f"32-byte hex: {token_32}")
print(f"Lengths: {len(token_16)}, {len(token_32)} characters")

Output:

16-byte hex: a3b7c9d1e5f20a4b6c8d0e2f4a6b8c0d
32-byte hex: f1a2b3c4d5e6f7089a0b1c2d3e4f50617a8b9c0d1e2f3a4b5c6d7e8f90a1b2c3
Lengths: 32, 64 characters

Each byte produces two hex characters, so token_hex(16) returns a 32-character string. For most applications, 32 bytes (256 bits) provides sufficient entropy.

URL-Safe Tokens with token_urlsafe()

URL-safe tokens use base64 encoding with - and _ instead of + and /. This makes them safe to include in URLs, query parameters, and HTTP headers without encoding issues.

# urlsafe_tokens.py
import secrets

token = secrets.token_urlsafe(32)
reset_link = f"https://example-app.com/reset?token={token}"

print(f"Token: {token}")
print(f"Reset link: {reset_link}")

Output:

Token: Rk9PQkFSLVRoaXMtaXMtYS1zYWZlLXRva2VuLXlheg
Reset link: https://example-app.com/reset?token=Rk9PQkFSLVRoaXMtaXMtYS1zYWZlLXRva2VuLXlheg

This is the most commonly used format for password reset links, email verification tokens, and API keys because it is compact and URL-compatible.

Raw Bytes with token_bytes()

When you need raw binary data — for encryption keys, HMACs, or feeding into other cryptographic functions — use token_bytes().

# raw_bytes.py
import secrets

raw = secrets.token_bytes(32)
print(f"Bytes: {raw}")
print(f"Length: {len(raw)} bytes")
print(f"As hex: {raw.hex()}")

Output:

Bytes: b'\xd4\x8a\x1f...(32 bytes of binary data)'
Length: 32 bytes
As hex: d48a1f3b7c9e2d...

Raw bytes are not human-readable, but they are the right choice when the token feeds directly into a cryptographic function like hmac.new() or a symmetric encryption library.

Generating Secure Passwords

The secrets module includes secrets.choice(), which selects a random element from a sequence using cryptographically secure randomness. Combined with Python’s string module, you can build password generators that meet any complexity requirement.

# password_gen.py
import secrets
import string

def generate_password(length=16):
    """Generate a secure password with mixed character types."""
    alphabet = string.ascii_letters + string.digits + string.punctuation
    
    # Ensure at least one of each required type
    password = [
        secrets.choice(string.ascii_uppercase),
        secrets.choice(string.ascii_lowercase),
        secrets.choice(string.digits),
        secrets.choice(string.punctuation),
    ]
    
    # Fill remaining length with random choices
    password += [secrets.choice(alphabet) for _ in range(length - 4)]
    
    # Shuffle to avoid predictable positions
    # Use secrets for the shuffle via sorting with random keys
    password.sort(key=lambda _: secrets.randbelow(1000))
    
    return ''.join(password)

# Generate 5 passwords
for i in range(5):
    pwd = generate_password(20)
    print(f"Password {i+1}: {pwd}")

Output:

Password 1: kQ7#mR2$nP9!xW4&bT6@
Password 2: jF3*hL8^vC1!yN5&dK7#
Password 3: wS6@tH4$pB9#mX2!qR8&
Password 4: gD1#zE5$cA3!oI7&uY9@
Password 5: fV8*aJ2^lG6!eM4&nW0#

The key detail is the shuffle step. Without it, the first four characters always follow the same type pattern (uppercase, lowercase, digit, punctuation). The secrets.randbelow() function provides a secure random integer that we use as a sort key to randomize character positions.

Comparing Tokens Securely

When verifying a token (checking if a submitted reset token matches the stored one), you must use constant-time comparison to prevent timing attacks. A timing attack measures how long the comparison takes — a regular == comparison returns False as soon as it finds the first mismatched character, leaking information about how many characters were correct.

# secure_compare.py
import secrets
import hmac

stored_token = "abc123def456"
submitted_token = "abc123def456"

# WRONG: vulnerable to timing attacks
if stored_token == submitted_token:
    print("Match (but insecure comparison)")

# RIGHT: constant-time comparison
if secrets.compare_digest(stored_token, submitted_token):
    print("Match (secure comparison)")

# Also works with bytes
stored_bytes = b"secret_token_value"
submitted_bytes = b"secret_token_value"
if secrets.compare_digest(stored_bytes, submitted_bytes):
    print("Bytes match (secure comparison)")

Output:

Match (but insecure comparison)
Match (secure comparison)
Bytes match (secure comparison)

The secrets.compare_digest() function always examines every character, taking the same amount of time regardless of where (or whether) the strings differ. This is the same function used internally by hmac.compare_digest().

Practical Security Patterns

Building a Password Reset Flow

Here is how to generate a time-limited password reset token with proper security practices:

# reset_flow.py
import secrets
import hashlib
import time

class TokenManager:
    """Manages secure, time-limited tokens."""
    
    def __init__(self, expiry_seconds=3600):
        self.tokens = {}  # In production, use a database
        self.expiry = expiry_seconds
    
    def create_token(self, user_id):
        """Generate a reset token for a user."""
        raw_token = secrets.token_urlsafe(32)
        # Store only the hash -- never store raw tokens
        token_hash = hashlib.sha256(raw_token.encode()).hexdigest()
        self.tokens[token_hash] = {
            'user_id': user_id,
            'created': time.time()
        }
        return raw_token  # Send this to the user via email
    
    def verify_token(self, submitted_token):
        """Verify a submitted token and return user_id if valid."""
        token_hash = hashlib.sha256(submitted_token.encode()).hexdigest()
        
        # Constant-time lookup using compare_digest
        for stored_hash, data in self.tokens.items():
            if secrets.compare_digest(stored_hash, token_hash):
                # Check expiry
                if time.time() - data['created'] > self.expiry:
                    del self.tokens[stored_hash]
                    return None  # Expired
                return data['user_id']
        return None  # Not found

# Usage
manager = TokenManager(expiry_seconds=3600)
token = manager.create_token("user_42")
print(f"Reset token: {token}")
print(f"Verified: {manager.verify_token(token)}")
print(f"Invalid token: {manager.verify_token('wrong-token')}")

Output:

Reset token: aB3cD4eF5gH6iJ7kL8mN9oP0qR1sT2uV3wX4yZ5
Verified: user_42
Invalid token: None

The critical detail is that we store only the hash of the token, never the raw token itself. If the database is breached, attackers get useless hashes. The raw token only exists in the email sent to the user and in memory during verification.

Generating API Keys

API keys typically need a prefix for identification and a random body for security:

# api_keys.py
import secrets

def generate_api_key(prefix="pk"):
    """Generate a prefixed API key."""
    random_part = secrets.token_urlsafe(24)
    return f"{prefix}_{random_part}"

# Generate different key types
public_key = generate_api_key("pk")
secret_key = generate_api_key("sk")
test_key = generate_api_key("tk")

print(f"Public key:  {public_key}")
print(f"Secret key:  {secret_key}")
print(f"Test key:    {test_key}")

Output:

Public key:  pk_Rk9PQkFSLVRoaXMtaXMtYQ
Secret key:  sk_c2VjcmV0LWtleS12YWx1ZQ
Test key:    tk_dGVzdC1rZXktdmFsdWUtaGVyZQ

The prefix makes it easy to identify key types in logs and configuration files without revealing the secret portion. This is the same pattern used by services like Stripe (sk_live_, pk_test_).

Real-Life Example: Secure Invitation System

Let us build a complete invitation system that generates secure invite codes, tracks their usage, and enforces expiration and single-use constraints.

# invitation_system.py
import secrets
import hashlib
import time
from datetime import datetime

class InvitationSystem:
    """Secure invitation code manager with expiry and usage tracking."""
    
    def __init__(self):
        self.invitations = {}
    
    def create_invite(self, creator, role="member", max_uses=1, 
                      expiry_hours=48):
        """Create a new invitation with constraints."""
        code = secrets.token_urlsafe(16)
        code_hash = hashlib.sha256(code.encode()).hexdigest()
        
        self.invitations[code_hash] = {
            'creator': creator,
            'role': role,
            'max_uses': max_uses,
            'current_uses': 0,
            'created': time.time(),
            'expiry': time.time() + (expiry_hours * 3600),
            'used_by': []
        }
        return code
    
    def redeem_invite(self, code, user_name):
        """Attempt to redeem an invitation code."""
        code_hash = hashlib.sha256(code.encode()).hexdigest()
        
        for stored_hash, invite in self.invitations.items():
            if not secrets.compare_digest(stored_hash, code_hash):
                continue
            
            if time.time() > invite['expiry']:
                return {"success": False, "error": "Invitation expired"}
            
            if invite['current_uses'] >= invite['max_uses']:
                return {"success": False, "error": "Invitation fully used"}
            
            invite['current_uses'] += 1
            invite['used_by'].append(user_name)
            return {
                "success": True,
                "role": invite['role'],
                "message": f"Welcome {user_name}! Role: {invite['role']}"
            }
        
        return {"success": False, "error": "Invalid invitation code"}
    
    def get_stats(self):
        """Get invitation usage statistics."""
        active = sum(1 for inv in self.invitations.values() 
                     if time.time() < inv['expiry'] 
                     and inv['current_uses'] < inv['max_uses'])
        expired = sum(1 for inv in self.invitations.values() 
                      if time.time() >= inv['expiry'])
        return {"active": active, "expired": expired, 
                "total": len(self.invitations)}

# Demo
system = InvitationSystem()

# Create invitations
admin_code = system.create_invite("alice", role="admin", max_uses=1)
team_code = system.create_invite("bob", role="editor", max_uses=5, 
                                  expiry_hours=72)

print(f"Admin invite: {admin_code}")
print(f"Team invite:  {team_code}")
print()

# Redeem invitations
print(system.redeem_invite(admin_code, "charlie"))
print(system.redeem_invite(admin_code, "dave"))  # Should fail
print(system.redeem_invite(team_code, "eve"))
print(system.redeem_invite("invalid-code", "mallory"))
print()

print(f"Stats: {system.get_stats()}")

Output:

Admin invite: xK7mN2pQ9rS1tUvW
Team invite:  aB3cD4eF5gH6iJ7k

{'success': True, 'role': 'admin', 'message': 'Welcome charlie! Role: admin'}
{'success': False, 'error': 'Invitation fully used'}
{'success': True, 'role': 'editor', 'message': 'Welcome eve! Role: editor'}
{'success': False, 'error': 'Invalid invitation code'}

Stats: {'active': 1, 'expired': 0, 'total': 2}

This system demonstrates several security best practices: hashed storage of codes, constant-time comparison, usage limits, and time-based expiration. In production, you would replace the in-memory dictionary with a database and add rate limiting to prevent brute-force code guessing.

Frequently Asked Questions

Can I just use random.SystemRandom() instead of secrets?

Yes, random.SystemRandom() also uses the OS entropy pool and is cryptographically secure. However, secrets is the recommended module since Python 3.6 because it provides a cleaner API specifically designed for security tasks. It also includes compare_digest() and token_urlsafe(), which SystemRandom does not.

How many bytes should my tokens be?

The Python documentation recommends at least 32 bytes (256 bits) for tokens used in security contexts. This provides sufficient entropy that brute-force guessing is infeasible even with massive computing resources. For lower-stakes use cases like email verification, 16 bytes (128 bits) is still very strong.

What happens if I call token_hex() with no arguments?

If you omit the byte count, secrets uses a default that is “reasonable for most use cases” — currently 32 bytes. However, it is better to specify the length explicitly so your code is self-documenting and immune to future default changes.

Is secrets slower than random?

Yes, but the difference is negligible for token generation. Generating 10,000 tokens with secrets takes about 50 milliseconds compared to 15 milliseconds with random. Since you typically generate tokens one at a time (not in bulk), the performance difference is irrelevant.

Should I use uuid4() or secrets for unique identifiers?

uuid.uuid4() generates random UUIDs using os.urandom(), so it is cryptographically secure. Use UUIDs when you need a standardized format (like database primary keys). Use secrets when you need a security token with a specific format (URL-safe, hex) or when you need compare_digest() for safe verification.

Conclusion

The secrets module gives you everything you need to generate cryptographically secure tokens, passwords, and random values in Python. We covered token_hex() for hexadecimal strings, token_urlsafe() for URL-compatible tokens, token_bytes() for raw binary data, and compare_digest() for timing-attack-resistant comparisons. We also built practical systems for password resets, API key generation, and invitation codes.

Try extending the invitation system with features like rate limiting, audit logging, or integration with a real database using SQLite or PostgreSQL. The secrets module is small but foundational — once you understand it, you can build secure authentication flows for any Python application.

For the full API reference, see the official Python secrets documentation.