Password Policies in Python — Core Concepts

Why password policies exist

Users choose terrible passwords. Studies consistently show that the most popular passwords are “123456”, “password”, and “qwerty.” Attackers maintain lists of billions of known passwords from breaches and run automated attacks against login forms. A password policy is your application’s defense against users inadvertently choosing passwords that an attacker would try first.

Modern vs. legacy policies

Traditional policies demanded complexity: uppercase, lowercase, digit, special character, minimum 8 characters. The result? Users created passwords like “Summer2024!” — technically compliant but utterly predictable.

NIST Special Publication 800-63B (2017, updated 2024) overturned the old advice:

Do require:

  • Minimum 8 characters (15+ for privileged accounts)
  • Checking against known breached password lists
  • Checking against context-specific words (username, site name, email)

Don’t require:

  • Mandatory complexity rules (uppercase, special characters)
  • Regular password rotation (unless there’s evidence of compromise)
  • Password hints or security questions

The reasoning: forced complexity leads to predictable patterns. Forced rotation leads to incrementing passwords (“Winter2024” → “Spring2024”). Both create a false sense of security.

Breach list checking

The most impactful policy check: is this password already known from a data breach? The “Have I Been Pwned” (HIBP) API uses a k-anonymity model — you send the first 5 characters of the password’s SHA-1 hash, and the API returns all hashes starting with that prefix. Your app checks locally whether the full hash appears in the response. The actual password never leaves your server.

import hashlib
import requests

def is_breached(password: str) -> bool:
    sha1 = hashlib.sha1(password.encode()).hexdigest().upper()
    prefix, suffix = sha1[:5], sha1[5:]

    response = requests.get(f"https://api.pwnedpasswords.com/range/{prefix}")
    return suffix in response.text

Strength estimation

Rather than binary pass/fail rules, modern libraries estimate password strength based on patterns, dictionary words, and keyboard sequences. The zxcvbn library (originally from Dropbox) provides a 0-4 score and estimated crack time:

from zxcvbn import zxcvbn

result = zxcvbn("correct horse battery staple")
# result["score"] = 4 (strongest)
# result["crack_times_display"]["offline_slow_hashing_1e4_per_second"] = "centuries"

result = zxcvbn("P@ssw0rd!")
# result["score"] = 0 (weakest)
# result["feedback"]["warning"] = "This is similar to a commonly used password"

Password hashing

Policy enforcement happens at input time. Storage is a separate concern: never store passwords in plaintext or with reversible encryption. Use a purpose-built hashing algorithm:

  • bcrypt — the standard choice, built-in work factor
  • argon2 — winner of the Password Hashing Competition, recommended for new projects
  • scrypt — memory-hard, good alternative

All three are intentionally slow, making brute-force attacks against stolen hashes impractical.

Common misconception

“Longer maximum password limits improve security.” Setting a maximum password length of 20 or 30 characters can actually hurt. Passphrases like “correct horse battery staple” need 28 characters. NIST recommends supporting at least 64 characters. If your hashing algorithm handles it (bcrypt truncates at 72 bytes, but argon2 doesn’t), there’s little reason to cap length below 128 characters.

The one thing to remember: Modern password policies focus on length and breach list checking rather than complexity rules — enforcing “at least 12 characters and not from a known breach” does more than requiring uppercase and special characters ever did.

pythonsecurityauthenticationweb

See Also