Skip to content

Random Password Generator

Most “password generator” tutorials online quietly teach you to write an insecure one. They use Python’s random module — which is not cryptographically secure. In this tutorial we will build the basic version first (so you understand the parts), then fix it with the right module (secrets), then go further with entropy calculations, customization flags, clipboard copy, a passphrase mode, and a Tkinter GUI.

You will learn:

  • Why random is wrong for passwords and secrets is right.
  • How to combine character sets (letters, digits, punctuation) cleanly.
  • How to guarantee that every required character type is present.
  • How to compute the entropy of a password and what that number means.
  • How to copy a generated password to the clipboard and clear it after a delay.
  • How to wrap the whole thing in a tiny GUI.

Almost every online tutorial uses:

insecure.py
import random
password = ''.join(random.choice(charset) for _ in range(length))   # ❌

The random module uses the Mersenne Twister PRNG. It is fast, statistically uniform, and completely predictable if you know its state. Given enough output an attacker can reconstruct the seed and predict every future call.

For anything security-related — passwords, tokens, session IDs, OTP codes — use secrets:

secure.py
import secrets, string
charset = string.ascii_letters + string.digits + string.punctuation
password = ''.join(secrets.choice(charset) for _ in range(length))   # ✅

The secrets module is a thin wrapper over your OS’s cryptographic randomness source (/dev/urandom on Unix, CryptGenRandom on Windows). It is what every standard-library example for cryptography uses.

  • Python 3.6 or above (secrets was added in 3.6).
  • A code editor or IDE.
  • Familiarity with strings, loops, and the input() function.
  1. Create a folder named random-password-generator.
  2. Inside it, create randompasswordgenerator.py.
Random Password Generator pch.viewSource
Random Password Generator
"""Random password generator — with `secrets`, not `random`.

`random` is seeded from the clock and its state can be reconstructed from a
handful of outputs. That is fine for a dice game and disqualifying for a
password. `secrets` draws from the operating system's CSPRNG, and switching
between them is a one-word edit, so there is no reason to get this wrong.

    python randompasswordgenerator.py            # a few passwords and their entropy
    python randompasswordgenerator.py --gui      # tkinter window, needs a display
    python randompasswordgenerator.py --test     # unit tests
"""

import math
import secrets
import string
import sys

ALPHABET = (string.ascii_lowercase + string.ascii_uppercase
            + string.digits + string.punctuation)

# The characters that look like each other in most fonts. Excluding them
# costs about 0.4 bits per character and saves the reader typing 1 for l.
AMBIGUOUS = "0Oo1lI"

POOLS = [
    string.ascii_lowercase,
    string.ascii_uppercase,
    string.digits,
    string.punctuation,
]


def generate(length: int = 16, no_ambig: bool = False,
             every_class: bool = True) -> str:
    """A password of `length` characters.

    With `every_class` the result is guaranteed to contain one character from
    each of the four pools -- which is what most password *rules* demand, and
    which very slightly *reduces* entropy by removing the all-lowercase
    outcomes from the sample space. The rules win anyway, because a password
    the site rejects has zero bits of useful strength.
    """
    pools = POOLS
    if no_ambig:
        pools = ["".join(c for c in pool if c not in AMBIGUOUS)
                 for pool in pools]
    alphabet = "".join(pools)

    if not every_class:
        return "".join(secrets.choice(alphabet) for _ in range(length))

    if length < len(pools):
        raise ValueError(
            f"length must be >= {len(pools)} to satisfy character-class rules")
    password = [secrets.choice(pool) for pool in pools]
    password += [secrets.choice(alphabet)
                 for _ in range(length - len(pools))]
    # Without the shuffle the first four characters are always
    # lower, upper, digit, symbol -- a pattern an attacker can exploit.
    secrets.SystemRandom().shuffle(password)
    return "".join(password)


def entropy_bits(password: str) -> float:
    """Guessing cost in bits, assuming the attacker knows the pool sizes."""
    pool = 0
    if any(c.islower() for c in password):
        pool += 26
    if any(c.isupper() for c in password):
        pool += 26
    if any(c.isdigit() for c in password):
        pool += 10
    if any(c in string.punctuation for c in password):
        pool += len(string.punctuation)
    return len(password) * math.log2(max(pool, 1))


# A short stand-in for the EFF long list. The real one has 7,776 words (5 dice
# rolls' worth, 12.9 bits each); this has enough to demonstrate the method
# and the printed entropy is computed from whichever list is actually loaded.
WORDS = """
acorn amber anchor apron atlas bacon badge bagel banjo barge beacon beetle
bishop blazer bonus bounty branch bridle bronze bucket bugle cactus camera
candle canvas carbon cargo carrot cedar cello chapel cherry chisel cider
cinder clover cobalt cocoa comet copper coral cotton cougar cradle crayon
crest crimson crystal cyclone dagger dahlia daisy dapper dazzle decoy denim
dingo dolphin domino donkey dragon drifter dynamo eagle ember emerald engine
falcon fable fennel ferret fiddle finch flamingo flannel flint fossil galaxy
gadget garnet gazelle geyser ginger glacier glider granite gravel grotto
guitar gumbo hammock harbor hazel helmet heron hickory hollow hornet husky
igloo indigo ingot ivory jacket jaguar jasmine jersey jigsaw jubilee juniper
kayak kettle keystone kimono kitten koala lagoon lantern lattice lemon lever
lichen lilac linen lobster locket lumber lyric magnet mango maple marble
marlin meadow mellow meteor mimosa mineral minnow mitten monsoon mosaic
""".split()


def passphrase(n: int = 5, words: list[str] | None = None) -> str:
    """Diceware: n words joined by hyphens.

    Each word contributes log2(len(words)) bits, so the strength depends on
    the list, not on the punctuation. Five words from the real EFF list is
    64.6 bits; five from the short list below is less, and the program says
    which it used rather than quoting the number it wishes were true.
    """
    words = words or WORDS
    return "-".join(secrets.choice(words) for _ in range(n))


def passphrase_bits(n: int = 5, words: list[str] | None = None) -> float:
    words = words or WORDS
    return n * math.log2(len(set(words)))


def clear_later(seconds: int = 30) -> None:
    """Wipe the clipboard after a delay, so the password does not linger.

    A password sitting in the clipboard survives every later paste, every
    clipboard manager, and often a screenshot tool. `pyperclip` is optional
    here: the point stands with or without it installed.
    """
    import threading

    def wipe():
        try:
            import pyperclip
            pyperclip.copy("")
            print("clipboard cleared")
        except ImportError:
            pass

    timer = threading.Timer(seconds, wipe)
    timer.daemon = True                # never hold the process open
    timer.start()
    return timer


def gui():
    """The generator behind a tkinter window."""
    import tkinter as tk

    root = tk.Tk()
    root.title("Password Generator")
    length_var = tk.StringVar(value="16")
    out_var = tk.StringVar()

    def gen():
        try:
            out_var.set(generate(int(length_var.get())))
        except ValueError as exc:
            out_var.set(str(exc))

    tk.Entry(root, textvariable=length_var).pack()
    tk.Button(root, text="Generate", command=gen).pack()
    tk.Entry(root, textvariable=out_var, width=40).pack()
    root.mainloop()


def main():
    print(f"alphabet: {len(ALPHABET)} characters "
          f"({len(ALPHABET) - len(AMBIGUOUS)} without the ambiguous ones)\n")
    print(f"{'password':40} {'bits':>6}")
    print("-" * 48)
    for length in (8, 12, 16, 24):
        password = generate(length)
        print(f"{password:40} {entropy_bits(password):6.1f}")
    readable = generate(16, no_ambig=True)
    print(f"{readable:40} {entropy_bits(readable):6.1f}   no ambiguous chars")

    phrase = passphrase(5)
    print(f"\npassphrase from a {len(set(WORDS))}-word list:")
    print(f"  {phrase}")
    print(f"  {passphrase_bits(5):.1f} bits "
          f"(the full 7,776-word EFF list would give "
          f"{5 * math.log2(7776):.1f})")


if __name__ == "__main__":
    if "--test" in sys.argv:
        import unittest

        class TestGenerator(unittest.TestCase):
            def test_length_is_respected(self):
                for length in (4, 16, 64):
                    self.assertEqual(len(generate(length)), length)

            def test_every_class_present(self):
                for _ in range(200):
                    p = generate(8)
                    self.assertTrue(any(c.islower() for c in p))
                    self.assertTrue(any(c.isupper() for c in p))
                    self.assertTrue(any(c.isdigit() for c in p))
                    self.assertTrue(any(c in string.punctuation for c in p))

            def test_too_short_rejected(self):
                with self.assertRaises(ValueError):
                    generate(3)

            def test_no_ambiguous(self):
                for _ in range(200):
                    p = generate(16, no_ambig=True)
                    self.assertFalse(set(p) & set(AMBIGUOUS))

            def test_not_predictable_ordering(self):
                # If the shuffle were missing, position 0 would always be
                # lowercase. Over 200 draws that is worth catching.
                firsts = [generate(8)[0] for _ in range(200)]
                self.assertTrue(any(not c.islower() for c in firsts))

        unittest.main(argv=sys.argv[:1], exit=False)
    elif "--gui" in sys.argv:
        gui()
    else:
        main()
command
C:\Users\Your Name\random-password-generator> python randompasswordgenerator.py
Random Password Generator
Length: 16
aB3#dE7&hI9@kL2$

Running the file exactly as it ships takes 0.1 s and prints:

python randompasswordgenerator.py
alphabet: 94 characters (88 without the ambiguous ones)
 
password                                   bits
------------------------------------------------
2=:R_9z3                                   52.4
q_*7pORI<U>Q                               78.7
NaaOx+6F*{U>"/ks                          104.9
2J*q3+EE12}"}J5wzf-}I1jw                  157.3
e4t,.&A$rqK2}h^7                          104.9   no ambiguous chars
 
passphrase from a 142-word list:
  magnet-cello-flint-lattice-flamingo
  35.7 bits (the full 7,776-word EFF list would give 64.6)

Read from the top: this is what runs when you execute the file, and which function calls which. It is generated from the code, so it cannot drift from it.

diagram Diagram mermaid
randompasswordgenerator.py
import secrets
import string
  • secrets for randomness.
  • string provides ready-made character classes (ascii_letters, digits, punctuation).
randompasswordgenerator.py
ALPHABET = string.ascii_letters + string.digits + string.punctuation

Concatenated strings — Python treats this as one big string of characters from which to pick.

randompasswordgenerator.py
def generate(length: int = 16) -> str:
    return "".join(secrets.choice(ALPHABET) for _ in range(length))

A generator expression inside "".join(...) is the idiomatic way to build a string of N characters.

randompasswordgenerator.py
length = int(input("Length: "))
print(generate(length))

A naive generator may produce a password with no digits. Many password rules require at least one of each type:

guarantee.py
import secrets, string
 
def generate(length: int = 16) -> str:
    if length < 4:
        raise ValueError("length must be ≥ 4 to satisfy character-class rules")
    pools = [
        string.ascii_lowercase,
        string.ascii_uppercase,
        string.digits,
        string.punctuation,
    ]
    # One mandatory character from each pool…
    password = [secrets.choice(pool) for pool in pools]
    # …then fill the rest from the combined alphabet
    alphabet = "".join(pools)
    password += [secrets.choice(alphabet) for _ in range(length - len(pools))]
    secrets.SystemRandom().shuffle(password)   # avoid predictable ordering
    return "".join(password)

Subtle point: do not just append one digit at the end — that is a known weakness. We shuffle the whole list so the required characters land in random positions.

Password entropy in bits = log₂(alphabet_size ** length).

Alphabet sizeLengthEntropy (bits)Comment
26 (lowercase only)8~38Crackable in minutes by a GPU
62 (letters + digits)12~71Reasonable
95 (full printable ASCII)16~104Strong
9520~131Excellent

A function to compute it:

entropy.py
import math
def entropy_bits(password: str) -> float:
    pool = 0
    if any(c.islower() for c in password): pool += 26
    if any(c.isupper() for c in password): pool += 26
    if any(c.isdigit() for c in password): pool += 10
    if any(c in string.punctuation for c in password): pool += len(string.punctuation)
    return len(password) * math.log2(max(pool, 1))
 
print(f"{entropy_bits(p):.1f} bits")

The conventional wisdom: aim for ≥ 80 bits for important accounts, ≥ 128 bits for master passwords / vault keys.

Running the file prints the bit count for each length it generates:

python randompasswordgenerator.py
5k4{<<bE                                   52.4
0qP-s9%C;(<Z                               78.7
|xd\-Z7{YZ51th9d                          104.9
+i9}x)OXH@fey1H\@Kb/Rz3^                  157.3

Every four characters buys about 26 bits, because the 94-character alphabet contributes log2(94) = 6.55 bits each. Excluding the six ambiguous characters costs almost nothing: an 88-character pool is 6.46 bits per character, so a 16-character password drops from 104.9 to 103.4 bits — 1.5 bits, in exchange for a password a human can read off a screen without guessing whether that is a one or an l.

The passphrase mode is the interesting comparison. Five words from the short built-in list gives 35.7 bits; five from the real 7,776-word EFF list gives 64.6. The strength is entirely in the size of the word list, not in the hyphens or the capitalisation — which is why “correct horse battery staple” is only good advice when the words were actually chosen at random from a large list.

Some characters are easy to confuse: 0 / O, 1 / l / I. Let the user opt out:

no_ambig.py
AMBIGUOUS = "0Oo1lI"
def generate(length, no_ambig=False):
    pool = ALPHABET
    if no_ambig:
        pool = "".join(c for c in pool if c not in AMBIGUOUS)
    return "".join(secrets.choice(pool) for _ in range(length))

Generated passwords are easier to use if they land directly in the clipboard. Two options:

install
pip install pyperclip
clipboard.py
import pyperclip
pyperclip.copy(password)
print("Copied to clipboard.")

For extra hygiene, clear the clipboard after 30 seconds:

clear.py
import threading
def clear_later():
    pyperclip.copy("")
threading.Timer(30, clear_later).start()
ProblemCauseFix
Uses random.choiceInsecure PRNGSwitch to secrets.choice
Required character placed at index 0 every timeBuilt the password by appending required charactersShuffle after assembly
int(input()) crashes on lettersNo validationWrap with try/except
Password always rejected by the siteMissing a required character classUse the “guarantee variety” version
Same password generated twice in a scriptReused a seeded randomNever seed secrets (it is not seedable)

Long, memorable, and high-entropy. Five random words from a word list of 7,776 is ~64 bits already.

passphrase.py
import secrets, pathlib
WORDS = pathlib.Path("eff_wordlist.txt").read_text().splitlines()
def passphrase(n=5):
    return "-".join(secrets.choice(WORDS) for _ in range(n))

EFF publishes a free word list specifically for this purpose.

batch.py
n = int(input("How many? "))
for _ in range(n):
    print(generate(16))

Reuse the entropy function above; rate as Weak / OK / Strong / Excellent.

cli
python randompasswordgenerator.py --length 20 --no-ambig --count 5
gui.py
import tkinter as tk
from tkinter import ttk
def gen():
    out_var.set(generate(int(length_var.get())))
root = tk.Tk()
length_var = tk.StringVar(value="16")
out_var = tk.StringVar()
tk.Entry(root, textvariable=length_var).pack()
tk.Button(root, text="Generate", command=gen).pack()
tk.Entry(root, textvariable=out_var, width=40).pack()
root.mainloop()

Append generated passwords (with a label) to an encrypted file using cryptography.fernet. See Personal Diary for the same encryption idea.

Use pip install qrcode to render the password as a QR code you can scan to a phone.

Send a SHA-1 prefix of the password to Have I Been Pwned’s k-anonymity API — if it appears in known breaches, regenerate.

  • Use secrets. Always. Forever.
  • Length matters more than complexity. A long passphrase beats a short symbol soup.
  • Guarantee character classes when sites demand them; do not hope randomness gives you one.
  • Shuffle after assembly to avoid predictable positions.
  • Never log generated passwords anywhere (stdout in a CI run, error message, debug print).
  • Copy via clipboard, do not save to a plain file. If you must persist, encrypt.
  • New account signups.
  • API key / token generation.
  • One-time temporary passwords for password resets.
  • Database / service credentials (developer ergonomics + safety).
  • Wi-Fi guest passwords printed on a slip.
  • Cryptographic randomness — why random is the wrong tool.
  • Entropy — turning intuition about password strength into a number.
  • String composition — building from character classes.
  • CLI ergonomics — flags, validation, clipboard integration.
  • Security mindset — small choices like “shuffle after assembly” matter.
  • Switch to secrets everywhere immediately.
  • Add clipboard copy with auto-clear.
  • Add a passphrase mode using the EFF word list.
  • Integrate with Have I Been Pwned for compromise checks.
  • Wrap in a GUI or build it into a real password manager (encrypted vault, master password, search).

You generated random passwords, then learned why “random” is the wrong word — and rebuilt the generator with secrets. You measured entropy, guaranteed variety, copied to the clipboard, and have a roadmap to a real password manager. Security is full of these small, mostly-invisible choices. The full source is on GitHub. Find more security-focused projects on Python Central Hub.

  • random is not for passwords. random.choice draws from a Mersenne Twister, which is deterministic and reconstructible: observe 624 outputs and you can predict every one that follows. Python ships secrets for exactly this, and it is a drop-in change — secrets.choice instead of random.choice.
  • Length beats complexity. A 16-character alphabet-only password has more possible values than an 8-character one drawn from all 94 printable characters. The exercise below prints both numbers rather than asserting it.
  • Every character drawn independently means no guarantees. This generator can legitimately return a password with no digit at all — which fails many password policies. If a policy must be met, the characters have to be drawn per-class and shuffled, not drawn uniformly and hoped over.
  • Printing a password to stdout puts it in your shell history, in any log the terminal writes, and in the scrollback of whoever is sharing your screen.
  • 16 characters drawn from letters, digits and punctuation — 94 candidates per position.
  • random.choice is predictable by design; secrets.choice is the one to use when the output is a credential.
  • Length contributes more entropy per character typed than alphabet size does, and the exercise measures by how much.
  • Independent draws mean the result satisfies no policy in particular, including ‘must contain a digit’.
  • ask() supplies 16 when nothing is typed, which is what makes the run above reproducible.
pch.quizTag pch.quizDefaultTitle
  1. Why is `random.choice` the wrong function for generating a password?

    pch.quizShowAnswer

    B — It draws from a Mersenne Twister, whose internal state can be reconstructed from enough observed output — so the sequence is predictable rather than secret

  2. Which gives more possible passwords: 8 characters from 94 symbols, or 16 characters from 52 letters?

    pch.quizShowAnswer

    B — The 16-character one, and by an enormous margin — length is an exponent while alphabet size is only the base

  3. This generator can return a password containing no digit. Is that a bug?

    pch.quizShowAnswer

    B — It is a consequence of drawing every character independently — correct for uniform randomness, and wrong if a policy has to be satisfied. Which one you want has to be decided, not assumed

pch.coffeeTagline

pch.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading