Python Secrets Token Generation — Core Concepts

Why a Dedicated Module Exists

Python 3.6 introduced secrets specifically because developers kept using random for security-sensitive tasks. The random module uses a Mersenne Twister PRNG — fast and statistically uniform, but deterministic. If an attacker observes 624 consecutive outputs, they can reconstruct the internal state and predict every future value. For simulations that’s irrelevant; for authentication tokens it’s catastrophic.

The secrets module wraps os.urandom(), which pulls from the operating system’s cryptographic random source (/dev/urandom on Linux, CryptGenRandom on Windows). These sources harvest entropy from hardware interrupts, disk timing, and other unpredictable physical events.

The Core Functions

secrets.token_bytes(n) — Returns n random bytes as a bytes object. Useful as raw key material or when you need to feed bytes into another cryptographic function.

secrets.token_hex(n) — Returns 2n hex characters. Common for database-stored tokens where you want a printable, fixed-length string.

secrets.token_urlsafe(n) — Returns a base64url-encoded string (roughly 1.3 × n characters). This is the go-to for anything that travels in URLs: password reset links, email verification tokens, OAuth state parameters.

secrets.choice(sequence) — Picks a single item from a sequence using a cryptographically secure source. Handy for generating passphrases from word lists or selecting random characters for a password.

secrets.randbelow(n) — Returns a random integer in [0, n). The secure equivalent of random.randrange(n).

How Much Entropy Is Enough?

The n parameter controls byte count, not string length. A 32-byte token has 256 bits of entropy — more than enough to resist brute-force attacks even with nation-state resources. The Python documentation recommends at least 32 bytes for tokens that must resist online and offline attacks.

For context: a 16-byte (128-bit) token would require an average of 2¹²⁷ guesses to crack. At a trillion guesses per second, that’s roughly 5 × 10¹⁸ years.

Common Patterns in Production

Password reset links — Generate a token_urlsafe(32), store its SHA-256 hash in the database alongside the user ID and an expiration timestamp. When the user clicks the link, hash the incoming token and compare. This way, a database leak doesn’t expose valid tokens.

API keys — Use token_hex(32) to create a 64-character key. Prefix it with a service identifier (e.g., sk_live_) so developers can identify which service a leaked key belongs to.

One-time codes (OTP) — For 6-digit numeric codes: secrets.randbelow(1_000_000) padded with str.zfill(6). Set a short TTL (5–10 minutes) and limit verification attempts to prevent brute-force.

Passphrase generation — Load a word list and call secrets.choice(words) repeatedly. Four words from a 7,776-word Diceware list yield about 51 bits of entropy — adequate for human-memorable passwords with rate limiting.

Common Misconception

Many developers believe random.SystemRandom() and secrets are interchangeable. They do share the same entropy source, but secrets is purpose-built with convenience functions for token generation and comparison. More importantly, using secrets signals intent: anyone reading the code understands this value must be unpredictable. Using random.SystemRandom leaves ambiguity about whether the developer considered security.

Timing-Safe Comparison

The module provides secrets.compare_digest(a, b), which compares two strings or bytes in constant time. Regular == comparison short-circuits on the first mismatched character, leaking information about how many characters matched. An attacker can exploit this timing difference to reconstruct a token character by character. Always use compare_digest when verifying tokens against stored values.

The one thing to remember: secrets is small by design — a handful of functions that do one job right. The hard part isn’t learning the API; it’s remembering to use it every time a value must stay unpredictable.

pythonsecuritycryptography

See Also