Skip to content
Python Basics: Generating Tokens with secrets

Python Basics: Generating Tokens with secrets

October 21, 2026

A simulation can use a repeatable random sequence. An access token should not be predictable from a demonstration seed. Python’s secrets module draws on operating-system randomness for security-sensitive values.

The examples below create fresh random values but print only deterministic properties, such as length and allowed characters. Your underlying values will differ. No token is printed, saved, sent to a server, or used to grant access.

Prepare a scratch directory

These three standalone examples were tested on Linux with Python 3.12.14. Use Bash, Python 3.12 or newer, a text editor, and the standard mktemp, rm, and rmdir utilities. No third-party packages are required. Keep assertions enabled by running without Python’s -O option.

Create a fresh directory and keep using the same terminal:

start_dir="$PWD"
work_dir="$(mktemp -d)" || exit 1
cd -- "$work_dir" || exit 1

Save each named source file here as UTF-8. Avoid naming a source file secrets.py. Assertions check formats and boundaries, not the statistical quality or uniqueness of the random source.

1. Generate an explicit number of random bytes

Save as make_hex_token.py:

import secrets

raw = secrets.token_bytes(32)
token = secrets.token_hex(32)
hex_digits = set("0123456789abcdef")

assert isinstance(raw, bytes)
assert len(raw) == 32
assert len(token) == 64
assert set(token) <= hex_digits
assert secrets.token_bytes(0) == b""
assert secrets.token_hex(0) == ""
print("Raw byte count:", len(raw))
print("Hex character count:", len(token))
print("Lowercase hex only:", set(token) <= hex_digits)
print("Zero-byte result:", repr(secrets.token_hex(0)))

Run it:

python3 make_hex_token.py

Output:

Raw byte count: 32
Hex character count: 64
Lowercase hex only: True
Zero-byte result: ''

The two calls generate independent values; token is not the hexadecimal encoding of raw. Each hexadecimal byte uses two characters, so requesting 32 random bytes produces 64 text characters. Passing the size explicitly keeps the example independent of the module’s changeable default.

The zero-byte case documents a boundary, not a useful access token. Keep the random-byte count separate from the displayed character count when setting application requirements.

2. Use a URL-safe representation

Save as make_url_token.py:

import secrets
import string

token = secrets.token_urlsafe(24)
allowed = set(string.ascii_letters + string.digits + "-_")

assert len(token) == 32
assert set(token) <= allowed
assert "=" not in token
assert secrets.token_urlsafe(0) == ""
print("Character count:", len(token))
print("URL-safe alphabet:", set(token) <= allowed)
print("Has padding:", "=" in token)
print("Empty request:", repr(secrets.token_urlsafe(0)))

Run it:

python3 make_url_token.py

Output:

Character count: 32
URL-safe alphabet: True
Has padding: False
Empty request: ''

token_urlsafe encodes random bytes into text suitable for URL components. Here, 24 bytes produce 32 characters without padding. Do not generalize that ratio to every byte count by rounding down; encoded lengths depend on the input size.

The allowed-character test inspects the actual generated value. We do not replace randomness with a seeded generator to make the output repeatable. Printing properties rather than secrets also keeps this tutorial’s terminal output safe to share.

3. Select an index within clear bounds

Save as choose_secure_index.py:

import secrets

labels = ("amber", "blue", "green")
index = secrets.randbelow(len(labels))
selected = secrets.choice(labels)

assert 0 <= index < len(labels)
assert labels[index] in labels
assert selected in labels
assert secrets.randbelow(1) == 0
print("Index within bounds:", 0 <= index < len(labels))
print("Choice is a known label:", selected in labels)
print("Only possible index:", secrets.randbelow(1))

try:
    secrets.randbelow(0)
except ValueError:
    print("Zero upper bound rejected")

Run it:

python3 choose_secure_index.py

Output:

Index within bounds: True
Choice is a known label: True
Only possible index: 0
Zero upper bound rejected

randbelow excludes its upper bound, which makes it convenient for sequence indexes. choice selects a value directly from a nonempty sequence. The independent calls need not select the same label. The deliberately invalid upper bound demonstrates the exception without terminating the script.

These three colors are toy data, not a password vocabulary. Repeated selections are allowed, and even large random tokens are not mathematically guaranteed to be unique. An application requiring uniqueness must detect collisions and retry.

Generating a token is only one part of an authentication feature. Expiration, single-use behavior, access controls, and safe storage require separate design. Do not put live tokens in screenshots or logs. Use random instead when reproducible simulations are the actual goal.

Clean up

In the same terminal, remove only these source files, return to the original directory, and remove the now-empty scratch directory:

rm -- make_hex_token.py make_url_token.py choose_secure_index.py
cd -- "$start_dir" || exit 1
rmdir -- "$work_dir"
unset start_dir work_dir

If rmdir reports that anything remains, inspect the directory rather than deleting unknown files.

References

Acknowledgements

This post was created with assistance from AI.

Last updated on