A cryptographically secure, policy-driven password and passphrase generator built on the secrets module with an argparse CLI.
Strong credentials must be generated with a cryptographically secure random source, not random. This project uses Python's secrets module to build passwords that satisfy a configurable policy — length, character classes, and guaranteed inclusion of each required class — plus a diceware-style passphrase mode. It teaches the difference between random and secrets, and how to reason about password entropy.
[!warning] Handle generated secrets carefully Treat generated passwords as sensitive: don't log them, echo them into shared terminals, or store them in plaintext. This tool is for your own account hardening and lab use.
- Generate passwords using a cryptographically secure random source.
- Guarantee required character classes without weakening randomness.
- Report entropy in bits and translate it into a verdict.
- Support passphrase generation as well as random strings.
- Avoid exposing generated secrets through logs, history, or arguments.
| Item | Detail |
|---|---|
| Python | 3.9 or newer |
| Dependencies | secrets, string, math, argparse (standard library) |
| Privileges | None |
| Wordlist | Optional, for passphrase mode — a large public list such as EFF's |
| Note | Uses secrets, never random |
- CLI —
argparsefor length, count, character-class toggles, and passphrase mode. - Policy builder — assembles the allowed alphabet from selected classes and records "required" classes.
- Generator — draws characters with
secrets.choice(); re-rolls until every required class appears. - Passphrase mode —
secrets.choice()over a word list joined by a separator. - Entropy estimator — reports approximate bits of entropy for the chosen policy.
policy (classes, length) ─▶ secrets.choice loop ─▶ satisfies policy? ─▶ output + entropy
password-generator/
├── passgen.py # CLI + generator
└── wordlist.txt # (optional) words for passphrase mode
- Generate. Build a password with
secrets.choice()over a defined alphabet. - Enforce classes. Guarantee at least one character from each required class, then shuffle with
secrets.SystemRandom().shuffle()so position is not predictable. - Compute entropy.
length * log2(alphabet_size)bits, reported alongside the password. - Add a verdict. Map entropy bands to labels and explain what would improve the score.
- Add passphrase mode. Select N words uniformly from a large wordlist; entropy is
N * log2(wordlist_size). - Support bulk generation with a
--countflag. - Protect the output. Never log generated values; support writing to a file with restrictive permissions.
#!/usr/bin/env python3
"""passgen.py - Cryptographically secure password/passphrase generator.
Uses secrets (CSPRNG), never random, for credential generation.
"""
import argparse
import math
import secrets
import string
CLASSES = {
"lower": string.ascii_lowercase,
"upper": string.ascii_uppercase,
"digits": string.digits,
"symbols": "!@#$%^&*()-_=+[]{};:,.?",
}
def build_alphabet(use: dict[str, bool]) -> tuple[str, list[str]]:
"""Return (alphabet, required_classes) from enabled character classes."""
alphabet = ""
required = []
for name, chars in CLASSES.items():
if use[name]:
alphabet += chars
required.append(chars)
if not alphabet:
raise ValueError("at least one character class must be enabled")
return alphabet, required
def generate_password(length: int, use: dict[str, bool]) -> str:
"""Generate a password guaranteeing at least one char per enabled class."""
alphabet, required = build_alphabet(use)
if length < len(required):
raise ValueError(f"length must be >= {len(required)} for this policy")
while True:
pw = "".join(secrets.choice(alphabet) for _ in range(length))
if all(any(c in cls for c in pw) for cls in required):
return pw
def generate_passphrase(words: list[str], count: int, sep: str) -> str:
"""Diceware-style passphrase from a word list."""
return sep.join(secrets.choice(words) for _ in range(count))
def entropy_bits(alphabet_size: int, length: int) -> float:
"""Approximate password entropy in bits: length * log2(alphabet)."""
return length * math.log2(alphabet_size)
def main() -> int:
parser = argparse.ArgumentParser(description="Secure password generator.")
parser.add_argument("-l", "--length", type=int, default=16, help="password length")
parser.add_argument("-c", "--count", type=int, default=1, help="how many to generate")
parser.add_argument("--no-lower", action="store_true", help="disable lowercase")
parser.add_argument("--no-upper", action="store_true", help="disable uppercase")
parser.add_argument("--no-digits", action="store_true", help="disable digits")
parser.add_argument("--no-symbols", action="store_true", help="disable symbols")
parser.add_argument("--passphrase", metavar="WORDLIST",
help="passphrase mode using the given word list file")
parser.add_argument("--words", type=int, default=5, help="words per passphrase")
args = parser.parse_args()
if args.passphrase:
with open(args.passphrase, encoding="utf-8") as fh:
words = [w.strip() for w in fh if w.strip()]
for _ in range(args.count):
print(generate_passphrase(words, args.words, "-"))
return 0
use = {
"lower": not args.no_lower,
"upper": not args.no_upper,
"digits": not args.no_digits,
"symbols": not args.no_symbols,
}
alphabet, _ = build_alphabet(use)
for _ in range(args.count):
print(generate_password(args.length, use))
print(f"[*] ~{entropy_bits(len(alphabet), args.length):.0f} bits of entropy "
f"(alphabet={len(alphabet)}, length={args.length})")
return 0
if __name__ == "__main__":
raise SystemExit(main())python passgen.py -l 20 -c 3
python passgen.py --passphrase /usr/share/dict/words --words 6K7!pv#Qm2@rLd9Xs&Tn0
b4$WcH8^uZ1yE-Rg3Jq!
xP2&mN9dV6@kL!sQ7wYt
[*] ~131 bits of entropy (alphabet=88, length=20)
# Single 20-character password
python3 pwgen.py --length 20
# Require every character class
python3 pwgen.py --length 24 --require-all
# Alphanumeric only, for systems that reject symbols
python3 pwgen.py --length 32 --no-symbols
# Six-word passphrase
python3 pwgen.py --passphrase --words 6 --wordlist eff_large_wordlist.txt
# Ten passwords, written with 0600 permissions
python3 pwgen.py --length 20 --count 10 --output creds.txt$ python3 pwgen.py --length 20 --require-all
7qF#vB2m!Kd8LzR@pTwX
Entropy: 131.1 bits (strong)
$ python3 pwgen.py --passphrase --words 6
sonnet-drapery-unwired-hexagon-tundra-glimpse
Entropy: 77.5 bits (strong)
$ python3 pwgen.py --length 6
kR2#mQ
Entropy: 39.3 bits (weak - increase length to at least 16)
| Condition | Exception | Response |
|---|---|---|
| Length below the class count | ValueError |
Explain that the password cannot satisfy every class; exit 2 |
| All character classes excluded | ValueError |
Report that the alphabet is empty; exit 2 |
| Wordlist file missing | FileNotFoundError |
Report the path and exit 2 |
| Wordlist too small | ValueError |
Warn that entropy will be low and refuse below a threshold |
| Output file not writable | PermissionError |
Report the path; never fall back to printing a secret unexpectedly |
| Negative or zero count | ValueError |
Validate in argparse with a range check |
[!warning] Handle generated credentials as live secrets Anything this tool produces should be treated as a real password from the moment it is generated.
secrets, neverrandom.randomis a Mersenne Twister seeded from predictable state; observing a few outputs lets an attacker reconstruct the sequence.secretsdraws from the OS CSPRNG.- Never log or print generated passwords to a shared terminal, and never write them into a report.
- Do not accept or emit passwords via command-line arguments — the command line is visible to every user through
psand persists in shell history. - Set restrictive permissions (
0600) on any output file, and prefer piping into a password manager over writing to disk. - Length beats composition. NIST SP 800-63B recommends long passphrases and a breach blocklist over forced symbol mixing.
- Entropy is an upper bound. A high-entropy string that already appears in a breach corpus is worthless — check against a blocklist too.
- Never reuse an example from documentation, including any shown here.
import math
import pwgen
def test_uses_secrets_not_random():
source = open(pwgen.__file__, encoding="utf-8").read()
assert "import secrets" in source
assert "import random" not in source
def test_generated_passwords_are_unique():
passwords = {pwgen.generate(20) for _ in range(1000)}
assert len(passwords) == 1000 # collisions at this length are implausible
def test_length_is_respected():
assert len(pwgen.generate(32)) == 32
def test_require_all_classes():
pw = pwgen.generate(16, require_all=True)
assert any(c.islower() for c in pw)
assert any(c.isupper() for c in pw)
assert any(c.isdigit() for c in pw)
assert any(not c.isalnum() for c in pw)
def test_entropy_matches_formula():
assert math.isclose(pwgen.entropy(20, 94), 20 * math.log2(94), rel_tol=1e-9)Cover: uniqueness, length, class guarantees, entropy arithmetic, and rejection of an impossible configuration.
- Pronounceable mode — generate syllable-based passwords for easier memorization.
- Breach check — offline-check candidates against a local Have I Been Pwned k-anonymity range set.
- Clipboard/zeroize — copy to clipboard and clear it after a timeout instead of printing.
- Policy presets —
--preset nist/--preset pcibundles for common requirements. - Ambiguity filter — option to exclude look-alike characters (
0/O,1/l/I).
| Symptom | Likely cause | Fix |
|---|---|---|
| Duplicate passwords across runs | random used instead of secrets |
Switch to secrets; random is seeded deterministically |
AttributeError: module 'secrets' has no attribute 'shuffle' |
secrets provides no shuffle |
Use secrets.SystemRandom().shuffle() |
| Password rejected by a target system | Symbols outside the accepted set | Use --no-symbols or a restricted alphabet |
| Entropy looks too high | Alphabet size overstated | Base it on the pool actually sampled from |
| Password mangled in the shell | Special characters interpreted by the shell | Quote in single quotes, or write to a file |
| Class guarantee weakens randomness | Required characters placed at fixed positions | Insert them, then shuffle the whole string |
- Python docs — secrets
- Python docs — string
- NIST SP 800-63B — Digital Identity Guidelines
- EFF — Diceware passphrases
- [[Hash-Cracker]]
- [[File-Integrity-Monitor]]
- [[Hashlib-Module]]
- [[Mini-Projects/Readme|Mini-Projects]] — module index
- [[Readme|Python for Security Professionals]] — course home