Password Strength Checker
Abstract
A password strength checker is more nuanced than it looks. Most tutorials count character classes and call it done — “has uppercase, has digit, has symbol — great password!” — which classifies Password1!Password1! as strong even though it appears in every leaked-credentials dump. In this project you will build the rule-based checker first, then layer on entropy estimation, a common-password blacklist, a leak-corpus check against Have I Been Pwned, and a final score modeled on zxcvbnzxcvbn.
You will leave understanding:
- Why character-class rules are necessary but not sufficient.
- How to compute password entropy.
- How
zxcvbnzxcvbnactually scores passwords. - How to query Have I Been Pwned safely using k-anonymity.
- How to give actionable feedback, not just a “weak/strong” verdict.
Prerequisites
- Python 3.6 or above.
- A code editor or IDE.
- Comfort with regex (
reremodule). - (Optional) Internet access for the HIBP check.
Getting Started
Create the project
- Create folder
password-strength-checkerpassword-strength-checker. - Inside, create
passwordstrengthchecker.pypasswordstrengthchecker.py.
Write the code
Password Strength Checker
Source# Password Strength Checker
import re
def password_strength_checker(password):
if len(password) < 8:
print("Password must be at least 8 characters long.")
elif not re.search("[a-z]", password):
print("Password must contain at least one lowercase letter.")
elif not re.search("[A-Z]", password):
print("Password must contain at least one uppercase letter.")
elif not re.search("[0-9]", password):
print("Password must contain at least one number.")
elif not re.search("[_@$]", password):
print("Password must contain at least one special character.")
else:
print("Password is strong.")
password_strength_checker("Password123")
password_strength_checker("Password")
password_strength_checker("password123")
password_strength_checker("PASSWORD123")
password_strength_checker("Password@")
password_strength_checker("Password123@")
password_strength_checker("Password123@!")
# Password Strength Checker
import re
def password_strength_checker(password):
if len(password) < 8:
print("Password must be at least 8 characters long.")
elif not re.search("[a-z]", password):
print("Password must contain at least one lowercase letter.")
elif not re.search("[A-Z]", password):
print("Password must contain at least one uppercase letter.")
elif not re.search("[0-9]", password):
print("Password must contain at least one number.")
elif not re.search("[_@$]", password):
print("Password must contain at least one special character.")
else:
print("Password is strong.")
password_strength_checker("Password123")
password_strength_checker("Password")
password_strength_checker("password123")
password_strength_checker("PASSWORD123")
password_strength_checker("Password@")
password_strength_checker("Password123@")
password_strength_checker("Password123@!")
Run it
C:\Users\Your Name\password-strength-checker> python passwordstrengthchecker.py
Password: Password123!
Strong (rule-based)C:\Users\Your Name\password-strength-checker> python passwordstrengthchecker.py
Password: Password123!
Strong (rule-based)Step-by-Step Explanation
1. The rule-based checker
import re
def rule_based(password: str) -> list[str]:
feedback = []
if len(password) < 8:
feedback.append("Use at least 8 characters.")
if not re.search(r"[a-z]", password):
feedback.append("Add a lowercase letter.")
if not re.search(r"[A-Z]", password):
feedback.append("Add an uppercase letter.")
if not re.search(r"[0-9]", password):
feedback.append("Add a digit.")
if not re.search(r"[^A-Za-z0-9]", password):
feedback.append("Add a symbol.")
return feedbackimport re
def rule_based(password: str) -> list[str]:
feedback = []
if len(password) < 8:
feedback.append("Use at least 8 characters.")
if not re.search(r"[a-z]", password):
feedback.append("Add a lowercase letter.")
if not re.search(r"[A-Z]", password):
feedback.append("Add an uppercase letter.")
if not re.search(r"[0-9]", password):
feedback.append("Add a digit.")
if not re.search(r"[^A-Za-z0-9]", password):
feedback.append("Add a symbol.")
return feedbackA list of issues is more useful than a single boolean — the user can fix multiple problems in one revision.
2. Strong vs. weak verdict
def strength(password: str) -> tuple[str, list[str]]:
issues = rule_based(password)
return ("Strong" if not issues else "Weak"), issuesdef strength(password: str) -> tuple[str, list[str]]:
issues = rule_based(password)
return ("Strong" if not issues else "Weak"), issues3. Run it
for p in ["Password", "password123", "PASSWORD123",
"Password@", "Password123@"]:
verdict, issues = strength(p)
print(f"{p}: {verdict}")
for i in issues: print(f" - {i}")for p in ["Password", "password123", "PASSWORD123",
"Password@", "Password123@"]:
verdict, issues = strength(p)
print(f"{p}: {verdict}")
for i in issues: print(f" - {i}")Why Rules Are Not Enough
Password1!Password1! satisfies every rule above. So does Qwerty12$Qwerty12$. They are also among the most common leaked passwords on Earth. Real strength assessment needs two more inputs:
- Common-password lists — reject anything in the top-N (try
rockyou.txtrockyou.txt, ~14 million). - Entropy — measure how much information the password actually carries.
Entropy Estimation
import math, string
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))import math, string
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))Rough rubric:
- < 28 bits — very weak (crackable in seconds).
- 28 – 35 — weak (offline crack in minutes).
- 36 – 59 — reasonable for low-value accounts.
- 60 – 127 — strong.
- ≥ 128 — paranoid / master-vault-grade.
A 16-character mix of all four classes lands around 105 bits — well into “strong” territory.
Common-Password Blacklist
The simplest meaningful upgrade. Get a list (e.g. SecLists’ 10-million-password-list-top-1000000.txt10-million-password-list-top-1000000.txt) and:
from pathlib import Path
COMMON = set(Path("top-passwords.txt").read_text().splitlines())
def is_common(password: str) -> bool:
return password.lower() in COMMONfrom pathlib import Path
COMMON = set(Path("top-passwords.txt").read_text().splitlines())
def is_common(password: str) -> bool:
return password.lower() in COMMONSet lookup is O(1). One million entries fits in ~30 MB of RAM — fine for a desktop tool.
Have I Been Pwned (k-Anonymity)
HIBP lets you check a password against billions of leaked entries without sending the password itself:
- SHA-1 your password.
- Send only the first 5 hex characters of the hash.
- Server returns all hashes starting with that prefix (about 500 per query).
- You check locally whether the full hash is in that list.
import hashlib, requests
def hibp_count(password: str) -> int:
sha1 = hashlib.sha1(password.encode("utf-8")).hexdigest().upper()
prefix, suffix = sha1[:5], sha1[5:]
r = requests.get(f"https://api.pwnedpasswords.com/range/{prefix}", timeout=10)
r.raise_for_status()
for line in r.text.splitlines():
s, count = line.split(":")
if s == suffix:
return int(count)
return 0import hashlib, requests
def hibp_count(password: str) -> int:
sha1 = hashlib.sha1(password.encode("utf-8")).hexdigest().upper()
prefix, suffix = sha1[:5], sha1[5:]
r = requests.get(f"https://api.pwnedpasswords.com/range/{prefix}", timeout=10)
r.raise_for_status()
for line in r.text.splitlines():
s, count = line.split(":")
if s == suffix:
return int(count)
return 0A non-zero count means the password appears in known breaches — reject it immediately. Even if it scores high on rules and entropy, attackers will try it first.
Putting It All Together
def full_check(password: str):
issues = rule_based(password)
bits = entropy_bits(password)
common = is_common(password)
try:
breaches = hibp_count(password)
except Exception:
breaches = None
score = 0
if len(password) >= 12: score += 1
if not issues: score += 1
if bits >= 60: score += 1
if not common: score += 1
if breaches == 0: score += 1
levels = ["Very Weak", "Weak", "Fair", "Good", "Strong", "Excellent"]
return {
"level": levels[score],
"bits": round(bits, 1),
"issues": issues,
"common": common,
"breaches": breaches,
}def full_check(password: str):
issues = rule_based(password)
bits = entropy_bits(password)
common = is_common(password)
try:
breaches = hibp_count(password)
except Exception:
breaches = None
score = 0
if len(password) >= 12: score += 1
if not issues: score += 1
if bits >= 60: score += 1
if not common: score += 1
if breaches == 0: score += 1
levels = ["Very Weak", "Weak", "Fair", "Good", "Strong", "Excellent"]
return {
"level": levels[score],
"bits": round(bits, 1),
"issues": issues,
"common": common,
"breaches": breaches,
}Now full_check("Password1!")full_check("Password1!") says “Weak — found in 1.6 million breaches” instead of “Strong — has all four character classes.”
Why You Should Recommend zxcvbnzxcvbn
For a real product use zxcvbn-pythonzxcvbn-python (a Python port of Dropbox’s library):
pip install zxcvbnpip install zxcvbnfrom zxcvbn import zxcvbn
r = zxcvbn("Tr0ub4dor&3")
print(r["score"], r["feedback"]["warning"], r["crack_times_display"])from zxcvbn import zxcvbn
r = zxcvbn("Tr0ub4dor&3")
print(r["score"], r["feedback"]["warning"], r["crack_times_display"])It models real attacker behavior — dictionary attacks, l33t substitutions, keyboard patterns, dates, repeats — and gives a 0-4 score plus an estimated time to crack at four attack scenarios. Your rule-based code is educational; zxcvbnzxcvbn is production.
Common Mistakes
| Problem | Cause | Fix |
|---|---|---|
Password1!Password1! rated strong | Only checked character classes | Add common-password + breach check |
| HIBP request fails | No internet | Make the breach check optional, surface as “unknown” |
| Sending password over the network | Naive API design | Use the k-anonymity range API |
| Locale-specific punctuation missed | Used a fixed regex | Use re.search(r"[^A-Za-z0-9]", password)re.search(r"[^A-Za-z0-9]", password) for “any non-alphanumeric” |
| Logs/print contain the password | Debugging left in | Never log passwords; never even print echoes |
| Comparing in plain text | Custom logic | Always hash before comparing/storing |
Variations to Try
1. Pattern detection
Reject keyboard runs (qwertyqwerty, asdfasdf), repeated characters (aaaaaa), and sequences (1234512345).
import re
def has_runs(p):
return bool(re.search(r"(.)\1\1", p)) or bool(re.search(r"012|123|234|abc|qwe", p.lower()))import re
def has_runs(p):
return bool(re.search(r"(.)\1\1", p)) or bool(re.search(r"012|123|234|abc|qwe", p.lower()))2. Dates and years
A 4-digit substring matching 19xx19xx or 20xx20xx is almost certainly a birth year — flag it.
3. Personal-info check
Accept the user’s name, email, and birth date; reject any password containing those.
4. GUI version
A Tkinter form with a colored progress bar that shifts red → yellow → green as the user types. See Calculator GUI for the layout pattern.
5. Web frontend
Flask + a textarea (see Basic Web Server). Never ship the password to the server unless you absolutely must; do the check in the browser with JS.
6. Estimated crack time
Multiply entropy by hashes-per-second assumptions:
- Online (1 000 guesses/s): how long to brute force?
- Offline GPU (1 trillion guesses/s): how long?
7. Suggest a fix
If the password is too short, append a random word. If a class is missing, insert one. If common, regenerate entirely.
8. Pair with the generator
Cross-reference with Random Password Generator. Show the strength of every generated password.
9. Account-wide policy
Read a config file specifying minimums (length, classes, entropy, max breach count) and enforce them.
10. CLI tool
strength check "Tr0ub4dor&3"
strength suggest --length 16 --no-ambigstrength check "Tr0ub4dor&3"
strength suggest --length 16 --no-ambigSecurity Best Practices the Tool Should Reinforce
- Length over complexity. A 16-character random passphrase beats
Aa1!Aa1!Aa1!Aa1!. - Unique per service. Even a strong password is weak if reused across breached sites.
- Never trust client-side checks alone. A web app must enforce policy on the server too.
- Treat the input as a secret. No logging, no caching, no analytics tracking the actual value.
- Hash + salt server-side. Never store passwords plaintext.
Real-World Applications
- Signup forms with real-time strength feedback.
- Password-change flows that reject reuse.
- IT admin tools that audit a company’s password hashes.
- Onboarding wizards that teach users why
Password1!Password1!is weak. - Self-service password reset with leak-corpus protection.
Educational Value
- Regex — pattern matching across character classes.
- Entropy math — turning intuition into a number.
- k-anonymity — privacy-preserving lookup design (a wonderful idea worth knowing).
- API integration — calling a real third-party service safely.
- UX of security — actionable feedback vs. red-X verdicts.
Next Steps
- Implement the common-password blacklist above.
- Add HIBP k-anonymity check.
- Switch to
zxcvbnzxcvbnfor production scoring. - Add pattern detection (sequences, runs, dates).
- Build a GUI with a colored progress bar.
- Pair with Random Password Generator for a complete password tooling pair.
Conclusion
You built a rule-based password checker, learned why the rules alone are dangerously incomplete, and added entropy + blacklist + breach-corpus checks that turn the tool into something you would actually trust. Real password security is a deceptively deep field, and even a small checker can do a real job — or quietly mislead users. Full source on GitHub. Find more security-focused projects on Python Central Hub.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
