Skip to content

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 zxcvbnzxcvbn actually 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 (rere module).
  • (Optional) Internet access for the HIBP check.

Getting Started

Create the project

  1. Create folder password-strength-checkerpassword-strength-checker.
  2. Inside, create passwordstrengthchecker.pypasswordstrengthchecker.py.

Write the code

Password Strength CheckerSource
Password Strength Checker
# 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
# 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

command
C:\Users\Your Name\password-strength-checker> python passwordstrengthchecker.py
Password: Password123!
Strong (rule-based)
command
C:\Users\Your Name\password-strength-checker> python passwordstrengthchecker.py
Password: Password123!
Strong (rule-based)

Step-by-Step Explanation

1. The rule-based checker

passwordstrengthchecker.py
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 feedback
passwordstrengthchecker.py
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 feedback

A list of issues is more useful than a single boolean — the user can fix multiple problems in one revision.

2. Strong vs. weak verdict

passwordstrengthchecker.py
def strength(password: str) -> tuple[str, list[str]]:
    issues = rule_based(password)
    return ("Strong" if not issues else "Weak"), issues
passwordstrengthchecker.py
def strength(password: str) -> tuple[str, list[str]]:
    issues = rule_based(password)
    return ("Strong" if not issues else "Weak"), issues

3. Run it

passwordstrengthchecker.py
for p in ["Password", "password123", "PASSWORD123",
          "Password@", "Password123@"]:
    verdict, issues = strength(p)
    print(f"{p}: {verdict}")
    for i in issues: print(f"  - {i}")
passwordstrengthchecker.py
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:

  1. Common-password lists — reject anything in the top-N (try rockyou.txtrockyou.txt, ~14 million).
  2. Entropy — measure how much information the password actually carries.

Entropy Estimation

entropy.py
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))
entropy.py
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:

blacklist.py
from pathlib import Path
COMMON = set(Path("top-passwords.txt").read_text().splitlines())
 
def is_common(password: str) -> bool:
    return password.lower() in COMMON
blacklist.py
from pathlib import Path
COMMON = set(Path("top-passwords.txt").read_text().splitlines())
 
def is_common(password: str) -> bool:
    return password.lower() in COMMON

Set 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:

  1. SHA-1 your password.
  2. Send only the first 5 hex characters of the hash.
  3. Server returns all hashes starting with that prefix (about 500 per query).
  4. You check locally whether the full hash is in that list.
hibp.py
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 0
hibp.py
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 0

A 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

full_check.py
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,
    }
full_check.py
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):

install
pip install zxcvbn
install
pip install zxcvbn
zxcvbn_use.py
from zxcvbn import zxcvbn
r = zxcvbn("Tr0ub4dor&3")
print(r["score"], r["feedback"]["warning"], r["crack_times_display"])
zxcvbn_use.py
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

ProblemCauseFix
Password1!Password1! rated strongOnly checked character classesAdd common-password + breach check
HIBP request failsNo internetMake the breach check optional, surface as “unknown”
Sending password over the networkNaive API designUse the k-anonymity range API
Locale-specific punctuation missedUsed a fixed regexUse re.search(r"[^A-Za-z0-9]", password)re.search(r"[^A-Za-z0-9]", password) for “any non-alphanumeric”
Logs/print contain the passwordDebugging left inNever log passwords; never even print echoes
Comparing in plain textCustom logicAlways hash before comparing/storing

Variations to Try

1. Pattern detection

Reject keyboard runs (qwertyqwerty, asdfasdf), repeated characters (aaaaaa), and sequences (1234512345).

pattern.py
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()))
pattern.py
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

cli
strength check "Tr0ub4dor&3"
strength suggest --length 16 --no-ambig
cli
strength check "Tr0ub4dor&3"
strength suggest --length 16 --no-ambig

Security 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 zxcvbnzxcvbn for 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 coffee

Was this page helpful?

Let us know how we did