Most password rules lie to users. "At least 8 characters, one uppercase, one number, one special character" — that regex check produces passwords like Password1! which scores 100% on every consumer-facing strength meter but cracks in minutes with a dictionary attack. Entropy-based validation gives you a number that actually correlates with cracking resistance.

What Is Password Entropy and Why It Matters

Entropy, measured in bits, tells you how many guesses an attacker needs in the worst case. The formula:

H = L × log₂(N)

Enter fullscreen mode Exit fullscreen mode

Where L is password length and N is the size of the character pool in use.

A password using only lowercase letters (N = 26) at 8 characters gives H = 8 × log₂(26) ≈ 37.6 bits. That means at most 2^37.6 ≈ 200 billion possible values — sounds large until you consider a GPU cluster doing 10 billion guesses per second. With full ASCII printable characters (N = 95) at 8 characters, you get ≈ 52.6 bits. Double the length to 16 characters: ≈ 105 bits. Practically unbreakable with current hardware.

The core insight: length beats complexity rules every time.

Detecting Character Pools

Before calculating entropy, determine which character sets the password draws from. More pools used means a larger N.

import math
import re

CHARSETS = {
    "lowercase": (re.compile(r'[a-z]'), 26),
    "uppercase": (re.compile(r'[A-Z]'), 26),
    "digits":    (re.compile(r'[0-9]'), 10),
    "special":   (re.compile(r'[^a-zA-Z0-9]'), 32),
}

def detect_charset_size(password: str) -> int:
    size = 0
    for _, (pattern, pool_size) in CHARSETS.items():
        if pattern.search(password):
            size += pool_size
    return size if size > 0 else 1

def calculate_entropy(password: str) -> float:
    n = detect_charset_size(password)
    length = len(password)
    if length == 0:
        return 0.0
    return length * math.log2(n)

Enter fullscreen mode Exit fullscreen mode

This returns theoretical maximum entropy — what you would get if the characters were drawn randomly from those pools. Real passwords are not random, so raw entropy overestimates strength. You need additional heuristic checks on top.

Building the Full Validator

Entropy alone misses practical weaknesses: common dictionary words, keyboard walk patterns (qwerty, asdf), repetitive characters. A production validator layers several signals.

from typing import NamedTuple

COMMON_PASSWORDS = {
    "password", "123456", "password1", "qwerty", "abc123",
    "letmein", "iloveyou", "admin", "welcome", "monkey",
    "dragon", "master", "sunshine", "princess", "football",
    "shadow", "superman", "michael", "jessica", "password123",
}

KEYBOARD_WALKS = ["qwerty", "asdf", "zxcv", "qazwsx", "1234", "abcd"]


class PasswordReport(NamedTuple):
    entropy: float
    strength: str        # "weak" | "fair" | "strong" | "very_strong"
    score: int           # 0-100
    issues: list[str]
    suggestions: list[str]


def validate_password(password: str) -> PasswordReport:
    issues: list[str] = []
    suggestions: list[str] = []
    penalty = 0

    entropy = calculate_entropy(password)

    if len(password) < 8:
        issues.append("Too short (minimum 8 characters)")
        suggestions.append("Use at least 12 characters for meaningful security")
        penalty += 40

    if password.lower() in COMMON_PASSWORDS:
        issues.append("This password appears in breach databases")
        suggestions.append("Avoid dictionary words and known patterns")
        penalty += 60

    for walk in KEYBOARD_WALKS:
        if walk in password.lower():
            issues.append(f"Contains keyboard walk pattern '{walk}'")
            suggestions.append("Avoid sequential keyboard patterns")
            penalty += 20
            break

    if len(set(password)) / max(len(password), 1) < 0.5:
        issues.append("Too many repeated characters")
        suggestions.append("Use a wider variety of characters")
        penalty += 15

    if entropy < 28:
        base_score = 10
    elif entropy < 36:
        base_score = 30
    elif entropy < 50:
        base_score = 55
    elif entropy < 70:
        base_score = 75
    elif entropy < 90:
        base_score = 90
    else:
        base_score = 100

    score = max(0, min(100, base_score - penalty))

    if score < 25:
        strength = "weak"
        if not suggestions:
            suggestions.append("Consider a passphrase: 4 random words beat most complex short passwords")
    elif score < 50:
        strength = "fair"
    elif score < 75:
        strength = "strong"
    else:
        strength = "very_strong"

    return PasswordReport(
        entropy=round(entropy, 2),
        strength=strength,
        score=score,
        issues=issues,
        suggestions=suggestions,
    )

Enter fullscreen mode Exit fullscreen mode

A few representative outputs:

  • "password" → 37.6 bits entropy, hit in common list → score 0, weak
  • "P@ssw0rd1!" → ~65 bits, no blocklist match → score 75, strong
  • "correct-horse-battery-staple" → 131+ bits, no issues → score 100, very_strong

That last one illustrates why passphrases win. The entropy formula rewards length above all else.

Exposing This as an API Endpoint

If you are validating passwords at registration, here is a minimal FastAPI endpoint:

from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()

class PasswordCheckRequest(BaseModel):
    password: str

class PasswordCheckResponse(BaseModel):
    entropy_bits: float
    strength: str
    score: int
    issues: list[str]
    suggestions: list[str]

@app.post("/validate-password", response_model=PasswordCheckResponse)
def check_password(req: PasswordCheckRequest) -> PasswordCheckResponse:
    report = validate_password(req.password)
    return PasswordCheckResponse(
        entropy_bits=report.entropy,
        strength=report.strength,
        score=report.score,
        issues=report.issues,
        suggestions=report.suggestions,
    )

Enter fullscreen mode Exit fullscreen mode

Two hard rules for this endpoint: never log the password itself (only the score and metadata), and always run this check server-side even if you also run it in the browser. Client-side validation is UX; server-side validation is security.

For production, complement the local blocklist with a Have I Been Pwned API lookup using the k-anonymity endpoint: send the first 5 hex characters of the SHA-1 hash, then check if the full hash appears in the returned list. That covers breached passwords your static list does not know about.

Passphrases: Counting Words, Not Characters

The character-based entropy formula underestimates passphrase strength. A 4-word Diceware passphrase drawn from a 7776-word list has log2(7776^4) ≈ 51.7 bits regardless of character count. Detect phrases and score them separately:

def passphrase_entropy(phrase: str) -> float:
    words = phrase.strip().split()
    if len(words) < 3:
        return 0.0
    # Conservative estimate: ~7776-word Diceware list
    return len(words) * math.log2(7776)

def smart_entropy(password: str) -> float:
    words = password.split()
    if len(words) >= 3:
        return passphrase_entropy(password)
    return calculate_entropy(password)

Enter fullscreen mode Exit fullscreen mode

"correct horse battery staple" yields 4 × 12.9 ≈ 51.7 bits — comparable to a strong random character password but far easier to remember and type correctly. If your UI currently bans spaces in passwords, remove that restriction.

The Takeaway

Entropy calculation gives you an honest measure of password strength — one tied to actual attack cost rather than checkbox compliance. The implementation above handles:

  • Theoretical entropy from character pool detection
  • Common password blocklist with penalty scoring
  • Keyboard walk pattern detection
  • Repetition penalty
  • Passphrase detection with word-level entropy
  • A clean 0–100 score with human-readable suggestions

If you are hardening an authentication system more broadly, check the security hardening checklists we publish — free PDF and Excel versions covering auth, API security, and container hardening.

The most impactful change most teams can make today: stop enforcing arbitrary complexity rules and set a minimum entropy threshold of 50 bits instead. Users get more flexibility, attackers face higher costs, and you stop training people to write Password1!.


I run AYI NEDJIMI Consultants, a cybersecurity consulting firm. We publish free security hardening checklists — PDF and Excel.