Project: Password Generator
1 · The lesson
readYou'll build a password generator that creates strong passwords. You can choose the length, the character types, and whether to leave out characters that look alike (like 0 and O). You'll also learn the security difference between the random and secrets modules.
What you'll practise: string module constants, random.choice, list comprehensions, functions with defaults, the secrets module (security-grade randomness).
Why this is useful: you can use this password generator for your own accounts. Most password managers have one built in, but here you will understand exactly how yours works.
Step 1: The Simplest Version
Pick N random characters from the letters of the alphabet.
import random import string length = 12 chars = string.ascii_letters # a-z and A-Z password = "".join(random.choice(chars) for _ in range(length)) print(password)
That's a 12-character random password. Run it a few times, and you get a different password each time.
string.ascii_letters is a constant in the string module: 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'. The string module has several of these:
import string print(string.ascii_lowercase) # 'abc...xyz' print(string.ascii_uppercase) # 'ABC...XYZ' print(string.digits) # '0123456789' print(string.punctuation) # '!"#$%&\'()*+,-./...'
Step 2: Mix Character Types
Real passwords mix letters, digits, and symbols.
import random import string length = 16 charset = string.ascii_letters + string.digits + string.punctuation password = "".join(random.choice(charset) for _ in range(length)) print(password)
Run it a few times. You'll see passwords like qK7$mPx@v9.Lne!2.
Step 3: Guarantee Each Category
There's a problem: by bad luck, random.choice() could give you 16 letters and no digits. Real password rules usually require "at least one of each".
The fix: pick one character from each required pool, fill the rest with random characters, then shuffle.
import random import string def generate_password(length=16, *, use_digits=True, use_symbols=True): """Generate a password of the given length, guaranteeing each requested type.""" pools = [string.ascii_lowercase, string.ascii_uppercase] if use_digits: pools.append(string.digits) if use_symbols: pools.append(string.punctuation) if length < len(pools): raise ValueError(f"length must be at least {len(pools)} to include one of each type") # 1. Take one mandatory char from each pool chars = [random.choice(p) for p in pools] # 2. Fill the rest from the combined pool combined = "".join(pools) chars.extend(random.choice(combined) for _ in range(length - len(pools))) # 3. Shuffle so the mandatory chars aren't always in the same positions random.shuffle(chars) return "".join(chars) # Test for _ in range(5): print(generate_password()) # Without symbols (some sites reject them) print(generate_password(use_symbols=False)) # Long, letters-only print(generate_password(20, use_symbols=False, use_digits=False))
Notice the * in the signature: def generate_password(length=16, *, use_digits=True, use_symbols=True). The * makes use_digits and use_symbols keyword-only. You have to write use_digits=False. You can't pass False as a positional argument. This makes each call easier to read.
Step 4: Exclude Confusing Characters
Some characters look almost the same, which makes a password hard to read and type: 0/O, 1/l/I, 5/S. Real password generators usually have an "avoid lookalikes" option.
import random import string AMBIGUOUS = "0O1lI5S2Z" def generate_password(length=16, *, use_digits=True, use_symbols=True, no_ambiguous=False): pools = [string.ascii_lowercase, string.ascii_uppercase] if use_digits: pools.append(string.digits) if use_symbols: pools.append(string.punctuation) if no_ambiguous: pools = [p.translate(str.maketrans("", "", AMBIGUOUS)) for p in pools] if length < len(pools): raise ValueError(f"length must be ≥ {len(pools)}") chars = [random.choice(p) for p in pools] combined = "".join(pools) chars.extend(random.choice(combined) for _ in range(length - len(pools))) random.shuffle(chars) return "".join(chars) # Compare print("With ambiguous: ", generate_password(16)) print("Without ambiguous: ", generate_password(16, no_ambiguous=True))
str.maketrans("", "", AMBIGUOUS) creates a translation table that deletes every character in AMBIGUOUS. We apply it to each pool to remove the lookalike characters.
Step 5: Use secrets Instead of random (Important!)
randomis for games and simulations.secretsis for passwords, tokens, and anything an attacker might want to predict.
The random module uses a deterministic algorithm: the same starting value (the seed) always gives the same sequence of numbers. An attacker who sees enough of your "random" outputs can work out the next ones. For passwords, that is a serious security hole.
The secrets module uses the operating system's secure random source, which is designed for cryptography.
import secrets import string def generate_secure_password(length=16, *, no_ambiguous=False): AMBIGUOUS = "0O1lI5S2Z" pools = [ string.ascii_lowercase, string.ascii_uppercase, string.digits, string.punctuation, ] if no_ambiguous: pools = [p.translate(str.maketrans("", "", AMBIGUOUS)) for p in pools] if length < len(pools): raise ValueError(f"length must be ≥ {len(pools)}") chars = [secrets.choice(p) for p in pools] combined = "".join(pools) chars.extend(secrets.choice(combined) for _ in range(length - len(pools))) # Shuffle securely too, because `random.shuffle` is not safe enough for secrets # We do it by picking new secure random positions n = len(chars) for i in range(n - 1, 0, -1): j = secrets.randbelow(i + 1) chars[i], chars[j] = chars[j], chars[i] return "".join(chars) print(generate_secure_password(20)) print(generate_secure_password(16, no_ambiguous=True))
Notice the manual Fisher-Yates shuffle with secrets.randbelow(). random.shuffle() isn't cryptographically secure, even if every character in the list was chosen with secrets.
Rule: any time the password could end up protecting something (an account, a vault, a session token), use secrets.
Step 6: Calculate Password Strength
How "strong" is a password? Roughly, it is the number of guesses an attacker needs to find it.
import math import string def password_entropy_bits(length, charset_size): """Return Shannon entropy in bits: log2(charset^length).""" return length * math.log2(charset_size) def describe_strength(bits): if bits < 40: return "weak (seconds to hours)" if bits < 60: return "moderate (hours to days)" if bits < 80: return "strong (months to years)" return "very strong (centuries+)" # Examples charsets = { "lowercase only": 26, "letters + digits": 62, "+ punctuation": 94, "letters only, no amb": 26 + 26 - 5, # AMBIGUOUS contains 5 letters: O, l, I, S, Z } for length in (8, 12, 16, 20): print(f"\nLength {length}:") for label, size in charsets.items(): bits = password_entropy_bits(length, size) print(f" {label:<22} {bits:>5.1f} bits → {describe_strength(bits)}")
The numbers show why length matters so much: each extra character multiplies the number of possible passwords by the size of the character set.
Stretch Goals
1. Memorable mode: instead of random characters, pick 4 random words from a dictionary (the XKCD method). Such a password is easier to remember, and often stronger than a short password full of symbols.
2. Pronounceable: alternate consonants and vowels so the password is sort-of speakable: "kalita-pen-44".
3. Pattern matching: a --pattern Lllll-dddd syntax to control structure precisely.
4. Save with labels: store generated passwords in a JSON file, with the site name as the key. (Encrypting the file with a master password is a much bigger project.)
5. Strength scorer: input any existing password and rate it.
🎯 Your Turn: Build a Password Strength Checker
Now do the reverse. Given an existing password, rate its strength.
import math, string def rate_password(password): """Return a tuple (entropy_bits, label). Steps: 1. Determine which character classes the password uses: lowercase, uppercase, digits, punctuation. 2. Compute the charset size as the SUM of the classes used. 3. Entropy = len(password) * log2(charset_size). 4. Map entropy to a label: <40 weak, <60 moderate, <80 strong, else very strong. """ # TODO 1: detect which classes are used (build a set or four booleans) # TODO 2: sum up the charset size from the classes # TODO 3: compute entropy bits # TODO 4: return (bits, label) pass # Test print(rate_password("password")) # weak print(rate_password("Password1")) # moderate print(rate_password("Pa$$w0rd!2024")) # very strong by this formula, but it is "password" with swaps: real crackers try those first print(rate_password("xK3#mP9!qLn2$vR4")) # very strong
Hint 1: Detecting classes with any()
any(c.islower() for c in password) → True if any character is lowercase. Same with .isupper(), .isdigit(). For punctuation: c in string.punctuation.
Hint 2: Mapping bits → label
A simple if/elif ladder:if bits < 40: return bits, "weak"
elif bits < 60: return bits, "moderate"
elif bits < 80: return bits, "strong"
else: return bits, "very strong"
Show full solution
import math import string def rate_password(password): has_lower = any(c.islower() for c in password) has_upper = any(c.isupper() for c in password) has_digit = any(c.isdigit() for c in password) has_punct = any(c in string.punctuation for c in password) charset = 0 if has_lower: charset += 26 if has_upper: charset += 26 if has_digit: charset += 10 if has_punct: charset += 32 if charset == 0 or len(password) == 0: return 0, "empty" bits = len(password) * math.log2(charset) if bits < 40: label = "weak" elif bits < 60: label = "moderate" elif bits < 80: label = "strong" else: label = "very strong" return round(bits, 1), label for pw in ["password", "Password1", "Pa$$w0rd!2024", "xK3#mP9!qLn2$vR4"]: bits, label = rate_password(pw) print(f" {pw:<20} {bits:>5} bits → {label}")
What You Learned
- The
stringmodule's constants (ascii_letters,digits,punctuation) random.choicefor picking from a collection- Keyword-only arguments (the
*in the signature) str.maketransandtranslatefor character substitution/deletion- The
secretsmodule for security-grade randomness, and why you need it for passwords - Entropy as a measure of password strength
Remember the most important rule from this project: never use random for passwords, tokens or anything else that must be secret. Use secrets.
Next: Word Frequency Counter.
Found this useful? Share it with a friend who’s learning Python.