Python ships with everything you need to generate secure passwords and tokens. No pip install, no third-party dependencies—just the standard library. In this article, we walk through 10 practical patterns for password generation, from the simplest random string to production-ready unique batch generation. Every snippet runs on vanilla Python 3.6+.
One quick ground rule before we start: for anything security-related, use secrets instead of random. The random module is designed for modeling and simulation, not for generating tokens or credentials. They look almost identical in code, which is exactly why the mistake is so common.
1. Basic Random Password
The simplest starting point. Pick characters at random from a mixed pool of lowercase, uppercase, digits, and a handful of symbols.
import secrets
def generate_password(length=16):
chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%"
return "".join(secrets.choice(chars) for _ in range(length))
if __name__ == "__main__":
for i in range(5):
print(f"Password {i+1}: {generate_password()}")
This is perfectly fine for many use cases, but there is no guarantee that every character type actually appears. A 16-character string could end up all-lowercase by sheer chance. If a service requires at least one digit or symbol, jump ahead to pattern 5.
2. Excluding Confusing Characters
Characters like 0 vs O, or 1 vs l are a recurring source of pain when passwords are read aloud, printed on paper, or squinted at in a tiny terminal font.
import secrets
import string
def generate_password(length=16, excluded="0OIl1"):
all_chars = string.ascii_letters + string.digits + "!@#$%"
chars = "".join(c for c in all_chars if c not in excluded)
if not chars:
raise ValueError("No usable characters left.")
return "".join(secrets.choice(chars) for _ in range(length))
if __name__ == "__main__":
for i in range(5):
print(f"Password {i+1}: {generate_password()}")
Removing characters shrinks the pool, which slightly reduces entropy per character. In practice, adding one or two extra characters to the length more than compensates. The real win is fewer support tickets that start with “is that a zero or an O?“
3. Prefixed Random Strings
Sometimes you need a recognizable prefix—USER_, APIKEY_, a project code—followed by a random tail. This is less of a password and more of an identifier with randomness baked in.
import secrets
import string
def generate_with_prefix(prefix="USER_", random_length=12):
chars = string.ascii_letters + string.digits
tail = "".join(secrets.choice(chars) for _ in range(random_length))
return prefix + tail
if __name__ == "__main__":
for i in range(5):
print(f"Key {i+1}: {generate_with_prefix()}")
Remember: the prefix adds zero entropy. A 6-character prefix plus 12 random characters is exactly as strong as 12 random characters—no more, no less. Prefixes are for humans reading logs, not for confusing attackers.
4. Bulk Prefixed Generation
The same idea, but generate a batch at once. Useful when onboarding a group of users or provisioning API keys.
import secrets
import string
def generate_batch(prefix="APIKEY_", random_length=16, count=5):
chars = string.ascii_letters + string.digits
return [
prefix + "".join(secrets.choice(chars) for _ in range(random_length))
for _ in range(count)
]
if __name__ == "__main__":
for i, key in enumerate(generate_batch(), 1):
print(f"Key {i}: {key}")
No duplicate check here. With 16 random alphanumeric characters the collision probability is astronomically low, but if you are generating millions of rows, pattern 10 is safer.
Also worth noting: the moment you generate a batch, the biggest risk shifts from “is the randomness good enough” to “where does this list end up.” A strong generator feeding into a plaintext spreadsheet on someone’s desktop is a very familiar horror story.
5. Guaranteed Character Mix
Many services enforce rules: at least one uppercase letter, one digit, one symbol. This pattern satisfies those constraints by reserving one slot per required type, then filling the rest at random and shuffling.
import secrets
import string
import random
def generate_strong_password(length=16):
if length < 4:
raise ValueError("length must be >= 4")
lower = string.ascii_lowercase
upper = string.ascii_uppercase
digits = string.digits
symbols = "!@#$%^&*"
parts = [
secrets.choice(lower),
secrets.choice(upper),
secrets.choice(digits),
secrets.choice(symbols),
]
all_chars = lower + upper + digits + symbols
parts += [secrets.choice(all_chars) for _ in range(length - 4)]
random.shuffle(parts)
return "".join(parts)
if __name__ == "__main__":
for i in range(5):
print(f"Password {i+1}: {generate_strong_password()}")
Sharp-eyed readers will notice that random.shuffle() is not from secrets. For password generation the impact is negligible—it only determines the position of already-random characters—but if it bothers you, you can replace it with a Fisher–Yates shuffle driven by secrets.randbelow().
A subtler mistake: hard-coding the required character into a fixed position (e.g., first char is always uppercase). That creates a pattern, and patterns are the opposite of what we are going for.
6. Human-Readable Passwords
Strip out the most confusing characters and drop the symbols entirely. The result is slightly weaker per character but dramatically easier to dictate over the phone, print on a welcome letter, or type on a phone keyboard.
import secrets
import string
def generate_readable(length=16):
excluded = "0OIl1"
chars = "".join(
c for c in string.ascii_letters + string.digits
if c not in excluded
)
return "".join(secrets.choice(chars) for _ in range(length))
if __name__ == "__main__":
for i in range(5):
print(f"Password {i+1}: {generate_readable()}")
Security is not just about theoretical bit-strength. A perfect 128-bit password that gets written on a sticky note because nobody can type it is less secure than a merely-good 80-bit password that people actually use properly. Context matters.
7. Word-Based Passphrases
String together random words with a separator. The resulting passphrase is long (good for entropy) and memorable (good for humans). This is the approach famously illustrated by XKCD #936.
import secrets
WORDS = [
"river", "cloud", "apple", "stone", "forest",
"ocean", "light", "shadow", "iron", "spark",
"maple", "crane", "drift", "ember", "frost",
"bloom", "cedar", "ridge", "pearl", "storm",
]
def generate_passphrase(word_count=4):
return "-".join(secrets.choice(WORDS) for _ in range(word_count))
if __name__ == "__main__":
for i in range(5):
print(f"Passphrase {i+1}: {generate_passphrase()}")
The sample word list here is deliberately small to keep the snippet readable. In production, use a much larger list—the EFF Diceware list has 7,776 words. With 4 words from that list, you get roughly 51 bits of entropy, which is decent for a human-typed master password.
The cardinal sin of passphrases: picking your own words. “correct-horse-battery-staple” stopped being a good password the moment it became the go-to example.
8. URL-Safe Tokens
Need a token you can embed in a URL without worrying about encoding? secrets.token_bytes plus Base64 URL-safe encoding gives you a clean alphanumeric-plus-dash-underscore string.
import secrets
import base64
def generate_token(byte_length=24):
raw = secrets.token_bytes(byte_length)
return base64.urlsafe_b64encode(raw).decode().rstrip("=")
if __name__ == "__main__":
for i in range(5):
print(f"Token {i+1}: {generate_token()}")
“URL-safe” means the characters play nicely with URLs. It does not mean the token is safe to expose publicly. Tokens in URLs can leak through browser history, server logs, the Referer header, and shoulder-surfing. Keep them short-lived when possible.
9. Custom Batch Generation
A straightforward utility: specify the length and count, get a list.
import secrets
import string
def generate_batch(length=16, count=5):
chars = string.ascii_letters + string.digits + "!@#$%"
return [
"".join(secrets.choice(chars) for _ in range(length))
for _ in range(count)
]
if __name__ == "__main__":
for i, pw in enumerate(generate_batch(length=20, count=5), 1):
print(f"Password {i}: {pw}")
When the count gets large, consider piping straight to a file rather than printing to a terminal. Terminal scroll buffers have a way of sticking around, and scrolling back through 500 passwords in a shared screen session is the kind of surprise nobody needs.
10. Unique Batch Generation
When you need to guarantee no duplicates—for example, issuing one-time codes to a list of users—collect results in a set until you have enough.
import secrets
import string
def generate_unique(length=16, count=10):
chars = string.ascii_letters + string.digits + "!@#$%"
result = set()
while len(result) < count:
result.add("".join(secrets.choice(chars) for _ in range(length)))
return list(result)
if __name__ == "__main__":
for i, pw in enumerate(generate_unique(length=16, count=5), 1):
print(f"Password {i}: {pw}")
With a 16-character alphanumeric+symbol pool, collisions are vanishingly unlikely. But if someone passes length=4, count=100000, the loop will start struggling as the set fills up relative to the total possibility space. A quick sanity check on the ratio never hurts.
In practice, the scarier problem with batch issuance is not duplicate passwords—it is losing track of which password belongs to which user. The random number generator is the easy part. The bookkeeping is where things go wrong.
Wrapping Up
All 10 patterns use only the Python standard library: secrets, string, random, and base64. No installs, no dependencies, no supply-chain risk.
Pick the pattern that fits your use case. For login passwords, pattern 5 (guaranteed mix) is usually the sweet spot. For API tokens, pattern 8 (URL-safe) is clean and practical. For human-typed master passwords, pattern 7 (passphrases) wins on usability.
And remember: generating a strong password is the easy part. Storing it safely, transmitting it securely, and rotating it on time is where the real work begins.

Leave a Reply