Build a secure password generator in Python step-by-step. Create random, strong passwords with customizable options, password strength checker, bulk generation, and a tkinter GUI version.
Build a secure, customizable password generator with multiple modes — from a simple CLI script to a full GUI application.
Project Overview
What you'll build: A password generator with strength checking and GUI
Skills practiced: secrets, string, random, functions, classes, tkinter
Estimated time: 2–3 hours
Step 1: Basic Password Generator
# password_v1.py — Simple password generator
import secrets
import string
def generate_password(length=16):
"""Generate a cryptographically secure random password."""
# Character sets
alphabet = string.ascii_letters + string.digits + string.punctuation
# Use secrets (cryptographically secure) instead of random
password = ''.join(secrets.choice(alphabet) for _ in range(length))
return password
# Test
for _ in range(5):
pwd = generate_password(16)
print(f"Generated: {pwd}")
Step 2: Customizable Generator
# password_v2.py — Customizable password generator
import secrets
import string
import re
def generate_password(
length=16,
use_uppercase=True,
use_lowercase=True,
use_digits=True,
use_symbols=True,
exclude_ambiguous=True, # Exclude 0, O, l, 1, etc.
custom_symbols="!@#$%^&*",
):
"""Generate a password with custom character requirements."""
if length < 4:
raise ValueError("Password length must be at least 4")
# Build character pool
pool = ""
required_chars = []
if use_lowercase:
chars = string.ascii_lowercase
if exclude_ambiguous:
chars = chars.replace('l', '').replace('o', '')
pool += chars
required_chars.append(secrets.choice(chars))
if use_uppercase:
chars = string.ascii_uppercase
if exclude_ambiguous:
chars = chars.replace('O', '').replace('I', '')
pool += chars
required_chars.append(secrets.choice(chars))
if use_digits:
chars = string.digits
if exclude_ambiguous:
chars = chars.replace('0', '').replace('1', '')
pool += chars
required_chars.append(secrets.choice(chars))
if use_symbols:
pool += custom_symbols
required_chars.append(secrets.choice(custom_symbols))
if not pool:
raise ValueError("At least one character type must be selected")
# Ensure minimum length to include all required types
remaining_length = length - len(required_chars)
if remaining_length < 0:
raise ValueError(f"Length must be at least {len(required_chars)}")
# Generate the rest
password_chars = required_chars + [secrets.choice(pool) for _ in range(remaining_length)]
# Shuffle to avoid predictable positions
secrets.SystemRandom().shuffle(password_chars)
return ''.join(password_chars)
# Test with different configurations
configs = [
{'length': 12, 'use_symbols': True, 'use_digits': True},
{'length': 8, 'use_uppercase': False, 'use_symbols': False},
{'length': 20, 'use_digits': True, 'exclude_ambiguous': False},
{'length': 16, 'custom_symbols': '@#$%'},
]
print("Password Examples:")
for config in configs:
pwd = generate_password(**config)
print(f" Config {config}: {pwd}")
Step 3: Password Strength Checker
# strength_checker.py — Check password strength
import re
import math
import string
def check_password_strength(password):
"""
Analyze password strength.
Returns:
dict with score (0-100), level, and feedback list
"""
score = 0
feedback = []
issues = []
# ─── Length checks ────────────────────────────────────
length = len(password)
if length < 6:
issues.append("Too short (minimum 6 characters)")
elif length < 8:
score += 10
feedback.append("Use at least 8 characters for better security")
elif length < 12:
score += 20
elif length < 16:
score += 30
else:
score += 40
# ─── Character type checks ────────────────────────────
has_lower = bool(re.search(r'[a-z]', password))
has_upper = bool(re.search(r'[A-Z]', password))
has_digit = bool(re.search(r'\d', password))
has_symbol = bool(re.search(r'[!@#$%^&*()_+\-=\[\]{};:\'",.<>?/\\|`~]', password))
char_types = sum([has_lower, has_upper, has_digit, has_symbol])
score += char_types * 10
if not has_lower: issues.append("Add lowercase letters")
if not has_upper: issues.append("Add uppercase letters")
if not has_digit: issues.append("Add numbers")
if not has_symbol: issues.append("Add symbols (@, #, $, etc.)")
# ─── Pattern checks ───────────────────────────────────
# Repeated characters
if re.search(r'(.)\1{2,}', password):
score -= 10
issues.append("Avoid repeating characters (aaa, 111)")
# Sequential characters
seq_lower = any(password[i:i+3] in string.ascii_lowercase for i in range(len(password)-2))
seq_upper = any(password[i:i+3] in string.ascii_uppercase for i in range(len(password)-2))
seq_digit = any(password[i:i+3] in string.digits for i in range(len(password)-2))
if seq_lower or seq_upper or seq_digit:
score -= 10
issues.append("Avoid sequential characters (abc, 123)")
# Common patterns
common_patterns = [
'password', 'qwerty', '123456', 'abc123', 'admin',
'letmein', 'welcome', 'monkey', 'dragon', 'master'
]
if any(p in password.lower() for p in common_patterns):
score -= 20
issues.append("Contains common password pattern")
# ─── Entropy calculation ──────────────────────────────
pool_size = 0
if has_lower: pool_size += 26
if has_upper: pool_size += 26
if has_digit: pool_size += 10
if has_symbol: pool_size += 32
if pool_size > 0:
entropy = math.log2(pool_size ** length)
else:
entropy = 0
if entropy > 60:
score += 10
# ─── Final score ──────────────────────────────────────
score = max(0, min(100, score))
if score >= 80:
level = '🟢 Very Strong'
color = 'green'
elif score >= 60:
level = '🟡 Strong'
color = 'yellow'
elif score >= 40:
level = '🟠 Moderate'
color = 'orange'
elif score >= 20:
level = '🔴 Weak'
color = 'red'
else:
level = '⚫ Very Weak'
color = 'dark'
return {
'score': score,
'level': level,
'color': color,
'entropy': round(entropy, 1),
'length': length,
'char_types': char_types,
'has_lower': has_lower,
'has_upper': has_upper,
'has_digit': has_digit,
'has_symbol': has_symbol,
'issues': issues,
'suggestions': feedback,
}
# Test strength checker
test_passwords = [
'password',
'P@ssw0rd',
'MyStr0ng!Pass#2026',
'x',
'aB3$aB3$aB3$aB3$',
'correct-horse-battery-staple',
]
for pwd in test_passwords:
result = check_password_strength(pwd)
print(f"\nPassword: {pwd!r}")
print(f" Score: {result['score']}/100 — {result['level']}")
print(f" Entropy: {result['entropy']} bits")
if result['issues']:
print(f" Issues: {', '.join(result['issues'][:2])}")
Step 4: Full CLI Application
# password_generator_cli.py — Full command-line password generator
import secrets
import string
import argparse
import sys
import json
import os
from datetime import datetime
# Import functions from above
# (In real project, put them in separate modules)
def generate_multiple_passwords(count, **kwargs):
"""Generate multiple passwords at once."""
return [generate_password(**kwargs) for _ in range(count)]
def save_passwords(passwords, filename=None):
"""Save generated passwords to a file."""
if not filename:
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
filename = f'passwords_{timestamp}.txt'
with open(filename, 'w') as f:
f.write(f"Generated Passwords — {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write("=" * 50 + "\n\n")
for i, pwd in enumerate(passwords, 1):
strength = check_password_strength(pwd)
f.write(f"{i:3}. {pwd} [{strength['level']}]\n")
return filename
def interactive_generator():
"""Interactive password generator with menu."""
print("\n" + "=" * 55)
print(" 🔐 Secure Password Generator")
print("=" * 55)
while True:
print("\n 1. Generate a password")
print(" 2. Generate multiple passwords")
print(" 3. Check password strength")
print(" 4. Generate passphrase")
print(" 5. Quit")
choice = input("\n Choose: ").strip()
if choice == '1':
print("\n --- Password Settings ---")
try:
length = int(input(" Length [16]: ").strip() or 16)
use_sym = input(" Include symbols? [Y/n]: ").strip().lower() != 'n'
use_dig = input(" Include digits? [Y/n]: ").strip().lower() != 'n'
no_ambig = input(" Exclude ambiguous characters? [Y/n]: ").strip().lower() != 'n'
except ValueError:
print(" Invalid input")
continue
pwd = generate_password(length=length, use_symbols=use_sym,
use_digits=use_dig, exclude_ambiguous=no_ambig)
strength = check_password_strength(pwd)
print(f"\n Generated: {pwd}")
print(f" Strength: {strength['score']}/100 — {strength['level']}")
print(f" Entropy: {strength['entropy']} bits")
# Copy to clipboard if pyperclip is available
try:
import pyperclip
pyperclip.copy(pwd)
print(" ✅ Copied to clipboard!")
except ImportError:
print(" 💡 Install pyperclip to auto-copy: pip install pyperclip")
elif choice == '2':
try:
count = int(input(" How many passwords? [5]: ").strip() or 5)
length = int(input(" Length each? [16]: ").strip() or 16)
except ValueError:
print(" Invalid input")
continue
passwords = generate_multiple_passwords(count, length=length)
print(f"\n Generated {count} passwords:")
for i, pwd in enumerate(passwords, 1):
s = check_password_strength(pwd)
print(f" {i:2}. {pwd} [{s['level']}]")
save = input("\n Save to file? [y/N]: ").strip().lower()
if save == 'y':
filename = save_passwords(passwords)
print(f" ✅ Saved to {filename}")
elif choice == '3':
pwd = input(" Enter password to check: ").strip()
if not pwd:
continue
result = check_password_strength(pwd)
print(f"\n Password: {'*' * len(pwd)} ({len(pwd)} chars)")
print(f" Score: {result['score']}/100")
print(f" Level: {result['level']}")
print(f" Entropy: {result['entropy']} bits")
print(f" Types: {'✓' if result['has_lower'] else '✗'} lower "
f"{'✓' if result['has_upper'] else '✗'} upper "
f"{'✓' if result['has_digit'] else '✗'} digit "
f"{'✓' if result['has_symbol'] else '✗'} symbol")
if result['issues']:
print(f" Issues: {', '.join(result['issues'])}")
elif choice == '4':
# Passphrase: 4-6 random words
words = ['correct', 'horse', 'battery', 'staple', 'purple',
'monkey', 'bicycle', 'yellow', 'sunset', 'quantum',
'galaxy', 'coffee', 'dragon', 'winter', 'thunder']
num_words = int(input(" Number of words? [4]: ").strip() or 4)
separator = input(" Separator [-]: ").strip() or '-'
selected = [secrets.choice(words) for _ in range(num_words)]
passphrase = separator.join(selected)
print(f"\n Passphrase: {passphrase}")
print(f" Strength: {check_password_strength(passphrase)['level']}")
elif choice == '5':
print(" 👋 Goodbye!")
break
else:
print(" ❌ Invalid choice.")
if __name__ == '__main__':
interactive_generator()
Step 5: GUI Version (tkinter)
# password_gui.py — Tkinter GUI Password Generator
import tkinter as tk
from tkinter import ttk, messagebox
import secrets
import string
class PasswordGeneratorGUI:
def __init__(self, root):
self.root = root
self.root.title("🔐 Password Generator")
self.root.geometry("480x580")
self.root.resizable(False, False)
self.root.configure(bg='#2b2b2b')
self._setup_vars()
self._build_ui()
def _setup_vars(self):
self.length_var = tk.IntVar(value=16)
self.use_upper = tk.BooleanVar(value=True)
self.use_lower = tk.BooleanVar(value=True)
self.use_digits = tk.BooleanVar(value=True)
self.use_symbols = tk.BooleanVar(value=True)
self.exclude_ambig = tk.BooleanVar(value=True)
self.password_var = tk.StringVar(value="Click Generate!")
self.strength_var = tk.StringVar(value="")
def _build_ui(self):
pad = {'padx': 20, 'pady': 5}
# Title
title = tk.Label(self.root, text="🔐 Password Generator",
font=('Arial', 18, 'bold'), bg='#2b2b2b', fg='#64B5F6')
title.pack(pady=15)
# Length slider
length_frame = tk.Frame(self.root, bg='#2b2b2b')
length_frame.pack(fill='x', **pad)
tk.Label(length_frame, text=f"Length:", bg='#2b2b2b', fg='white',
font=('Arial', 11)).pack(side='left')
self.length_label = tk.Label(length_frame, textvariable=tk.StringVar(value='16'),
bg='#2b2b2b', fg='#64B5F6', font=('Arial', 11, 'bold'))
self.length_label.pack(side='right')
self.slider = tk.Scale(self.root, from_=6, to=64, orient='horizontal',
variable=self.length_var, bg='#2b2b2b', fg='white',
troughcolor='#555', highlightthickness=0, length=400,
command=lambda v: self.length_label.config(text=v))
self.slider.pack(**pad)
# Checkboxes
options_frame = tk.LabelFrame(self.root, text="Options", bg='#2b2b2b', fg='white',
font=('Arial', 10, 'bold'), padx=10, pady=5)
options_frame.pack(fill='x', padx=20, pady=10)
options = [
('Uppercase (A-Z)', self.use_upper),
('Lowercase (a-z)', self.use_lower),
('Digits (0-9)', self.use_digits),
('Symbols (!@#$)', self.use_symbols),
('Exclude Ambiguous (0, O, l, 1)', self.exclude_ambig),
]
for i, (text, var) in enumerate(options):
cb = tk.Checkbutton(options_frame, text=text, variable=var,
bg='#2b2b2b', fg='white', selectcolor='#555',
activebackground='#2b2b2b', activeforeground='white',
font=('Arial', 10))
cb.grid(row=i//2, column=i%2, sticky='w', padx=5, pady=2)
# Password display
pwd_frame = tk.Frame(self.root, bg='#2b2b2b')
pwd_frame.pack(fill='x', padx=20, pady=10)
self.pwd_display = tk.Entry(pwd_frame, textvariable=self.password_var,
font=('Courier', 13), bg='#1e1e1e', fg='#4CAF50',
justify='center', state='readonly', relief='flat',
readonlybackground='#1e1e1e')
self.pwd_display.pack(fill='x', ipady=10)
# Strength bar
self.strength_label = tk.Label(self.root, textvariable=self.strength_var,
font=('Arial', 11), bg='#2b2b2b', fg='white')
self.strength_label.pack()
self.progress = ttk.Progressbar(self.root, length=400, mode='determinate', maximum=100)
self.progress.pack(padx=20, pady=5)
# Buttons
btn_frame = tk.Frame(self.root, bg='#2b2b2b')
btn_frame.pack(pady=15)
for text, cmd, color in [
('🎲 Generate', self._generate, '#2196F3'),
('📋 Copy', self._copy, '#4CAF50'),
('💾 Save', self._save, '#FF9800'),
]:
tk.Button(btn_frame, text=text, command=cmd,
font=('Arial', 11, 'bold'), bg=color, fg='white',
width=10, relief='flat', cursor='hand2').pack(side='left', padx=5)
def _generate(self):
try:
pwd = generate_password(
length=self.length_var.get(),
use_uppercase=self.use_upper.get(),
use_lowercase=self.use_lower.get(),
use_digits=self.use_digits.get(),
use_symbols=self.use_symbols.get(),
exclude_ambiguous=self.exclude_ambig.get(),
)
self.password_var.set(pwd)
result = check_password_strength(pwd)
self.progress['value'] = result['score']
self.strength_var.set(f"{result['level']} | Score: {result['score']}/100 | Entropy: {result['entropy']} bits")
except ValueError as e:
messagebox.showerror("Error", str(e))
def _copy(self):
pwd = self.password_var.get()
if pwd == "Click Generate!":
messagebox.showwarning("No Password", "Generate a password first!")
return
self.root.clipboard_clear()
self.root.clipboard_append(pwd)
messagebox.showinfo("Copied!", "Password copied to clipboard!")
def _save(self):
pwd = self.password_var.get()
if pwd == "Click Generate!":
messagebox.showwarning("No Password", "Generate a password first!")
return
with open('saved_passwords.txt', 'a') as f:
f.write(f"{pwd}\n")
messagebox.showinfo("Saved!", "Password appended to saved_passwords.txt")
if __name__ == '__main__':
root = tk.Tk()
app = PasswordGeneratorGUI(root)
root.mainloop()
Summary
In this project, you built:
- ✅ V1: Basic password generator using
secrets - ✅ V2: Customizable generator with character set options
- ✅ V3: Password strength checker with entropy analysis
- ✅ V4: Full CLI interactive generator with save/copy
- ✅ V5: tkinter GUI with strength bar
Key skills practiced:
secrets module (cryptographic randomness)string module character sets- Regular expressions (
re) for pattern detection - Function parameters and defaults
- tkinter for GUI
- Math for entropy calculation
*Next Project: Expense Tracker →*