A password strength regex is a regular expression that enforces password complexity rules (minimum length and required character classes) at input time, before the value reaches the database. The most common production pattern is ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$, which requires at least one lowercase letter, one uppercase letter, one digit, one special character, and a minimum length of 8.
The pattern works because each rule is a (?=...) lookahead, a zero-width assertion that checks "does this exist anywhere?" without consuming characters. That lets you combine or relax rules without rewriting the whole expression. Below I walk the building blocks (length, character classes, lookahead-enforced requirements), runnable code in JavaScript, Python, and PHP, engine notes, common bugs, and the cases where regex is the wrong tool for the job.
Two things to remember before you ship a password regex: short, complex passwords are weaker than long, simple ones (Tr0ub4dor&3 is famously worse than correct horse battery staple), and your regex enforces format only. Storing the hash safely is a separate problem.
Quick reference
Minimum 8 characters, no other rules:
^.{8,}$
8+ characters with at least one lowercase, one uppercase, one digit:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$
The full "strong" pattern (lower, upper, digit, special, 8+):
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$
NIST 800-63B style (8+ characters, no other rules, server-side rejects breached passwords):
^.{8,}$
Length-only patterns
Start simple. Length is the most important factor in password strength.
Exactly 8 chars: ^.{8}$
8 to 10 chars: ^.{8,10}$
8 or more chars: ^.{8,}$
12 or more chars: ^.{12,}$
The . matches any character except newline. To exclude whitespace as well, use ^\S{8,}$. To allow only specific characters, replace . with a character class:
Lowercase letters, 8+: ^[a-z]{8,}$
Letters (upper/lower), 8+: ^[a-zA-Z]{8,}$
Alphanumeric, 8+: ^[a-zA-Z0-9]{8,}$
These restrict the allowed characters but say nothing about which classes must appear. For that, you need lookaheads.
Lookaheads for "must contain"
A lookahead (?=...) is a zero-width assertion: it checks the rest of the pattern can be satisfied starting from the current position, without consuming any characters. Chained at the start of a pattern, lookaheads enforce "must contain" rules without restricting the order in which characters appear.
Require at least one uppercase letter:
^(?=.*[A-Z])[a-zA-Z]{8,}$
Reading left to right:
^anchors to the start.(?=.*[A-Z])is the lookahead. It scans the whole string (because.*is greedy) looking for at least one uppercase letter. If found, the lookahead passes; the engine doesn't advance.[a-zA-Z]{8,}$then matches the actual content: 8 or more letters.
Require at least one lowercase AND one uppercase:
^(?=.*[a-z])(?=.*[A-Z])[a-zA-Z]{8,}$
Two lookaheads, both must succeed.
Require lowercase, uppercase, AND digit:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z0-9]{8,}$
Add a fourth class (special characters):
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{8,}$
Or accept any non-word character as "special":
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$
The . at the end is intentional: it lets the password contain any character (including Unicode), as long as the four lookahead rules pass. This is the version I prefer in practice.
For more on how lookahead semantics work, see regex lookaheads and lookbehinds.
Counting required occurrences
To require N occurrences of a character class, use a counted lookahead. Require at least 3 uppercase letters:
^(?=(?:.*[A-Z]){3}).{8,}$
The (?:.*[A-Z]){3} is "any characters, then uppercase letter" repeated 3 times. This counts non-overlapping occurrences.
To require exactly 3 uppercase letters (no more), pair the positive lookahead with a negative lookahead:
^(?=(?:.*[A-Z]){3})(?!(?:.*[A-Z]){4}).{8,}$
The negative lookahead (?!(?:.*[A-Z]){4}) fails if 4 or more uppercase letters can be found. This is the trick most "exactly N" patterns use.
Require minimum 3 lowercase AND minimum 2 uppercase:
^(?=(?:.*[a-z]){3})(?=(?:.*[A-Z]){2}).{8,}$
Forbidden patterns
To reject specific weak patterns, add negative lookaheads.
Reject passwords containing the word "password":
^(?!.*password).{8,}$
Reject passwords that are entirely numeric:
^(?!^\d+$).{8,}$
Reject passwords where the same character repeats 3+ times in a row:
^(?!.*(.)\1{2}).{8,}$
The (.)\1{2} matches any character followed by the same character twice more (using a backreference). Wrapped in a negative lookahead, it rejects passwords containing aaa, 111, !!!, etc.
Examples in JavaScript, Python, and PHP
JavaScript:
const STRONG = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$/;
function isStrongPassword(input) {
return STRONG.test(input);
}
isStrongPassword("Password1!"); // true
isStrongPassword("password1!"); // false (no uppercase)
isStrongPassword("PASSWORD1!"); // false (no lowercase)
isStrongPassword("Password!"); // false (no digit)
isStrongPassword("Password11"); // false (no special)
isStrongPassword("Pa1!"); // false (too short)Python:
import re
STRONG = re.compile(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$")
def is_strong_password(value: str) -> bool:
return bool(STRONG.match(value))
is_strong_password("Password1!") # True
is_strong_password("short1A!") # True (exactly 8 chars, all classes)
is_strong_password("alllowercase1!") # False (no uppercase)PHP:
function isStrongPassword(string $value): bool {
$pattern = '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$/';
return (bool) preg_match($pattern, $value);
}
isStrongPassword("Password1!"); // true
isStrongPassword("nodigit!Aaaa"); // false (no digit)Engine compatibility
Lookaheads are the key feature these patterns use. Every modern engine supports them, but the lookbehind variant (for "must NOT end with a digit" style rules) is not as widely supported.
| Engine | Lookahead (?=...) | Negative lookahead (?!...) | Lookbehind (?<=...) | Unicode-aware \d/\w |
|---|---|---|---|---|
| JavaScript | Works | Works | Works (ES2018+) | \d is ASCII; \p{L} Unicode (with u flag) |
Python (re) | Works | Works | Works (fixed-width only) | \w is Unicode by default, \d ASCII unless re.UNICODE |
Python (regex pkg) | Works | Works | Works (variable-width) | Full Unicode by default |
| PHP (PCRE) | Works | Works | Works | ASCII unless (*UCP) mode |
| Java | Works | Works | Works (fixed-width) | ASCII by default, Unicode with Pattern.UNICODE_CHARACTER_CLASS |
| .NET | Works | Works | Works (variable-width) | Unicode-aware by default for \w, ASCII for \d unless ECMAScript option |
| Go (RE2) | Not supported | Not supported | Not supported | ASCII |
Rust (regex crate) | Not supported | Not supported | Not supported | ASCII |
| Ruby | Works | Works | Works | Unicode by default |
POSIX ERE (grep -E) | Not supported | Not supported | Not supported | ASCII |
For Go and Rust, the lookahead-based "must contain" patterns do not compile. The workaround is to run separate regexes per requirement and combine the booleans:
hasLower := regexp.MustCompile(`[a-z]`).MatchString(s)
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(s)
hasDigit := regexp.MustCompile(`\d`).MatchString(s)
hasSpecial := regexp.MustCompile(`[^\w\s]`).MatchString(s)
isStrong := len(s) >= 8 && hasLower && hasUpper && hasDigit && hasSpecialThis is also the cleaner code style anyway: the regex stays simple, the rules are visible at the call site.
Common mistakes
The bugs I've shipped or seen in code review.
Forgetting the anchors. A pattern without ^ and $ (or \A/\z) accepts a 20-character password that contains a valid 8-character substring. Always anchor for validation.
Putting the character-class restriction inside the lookahead's content. A pattern like (?=[a-z]*[A-Z]) requires the uppercase letter to be after zero or more lowercase letters, which silently rejects passwords starting with the uppercase letter. Use (?=.*[A-Z]) (any chars, then uppercase) so order doesn't matter.
Trying to reject Unicode by accident. A pattern that uses [a-zA-Z] rejects Über1! because Ü isn't in the class. If you want to accept Unicode, use . for the content match and let the lookaheads enforce the rules. The pattern in this article does exactly that.
Letting the regex enforce a maximum length. A maximum-length cap (^.{8,64}$) is fine, but rejecting long passwords (e.g., ^.{8,16}$) actively hurts security. NIST 800-63B recommends accepting passwords up to at least 64 characters.
Validating the password but not the storage. A regex confirms the password is strong on input. It does nothing about storage. Hash with bcrypt, scrypt, or argon2 (never MD5 or SHA-256 alone). For application-specific guides, see the storage notes below.
Treating regex as a substitute for breach checks. A user can set a "strong-looking" password like Password1! that's in every breach corpus. For real security, check the password against the HaveIBeenPwned k-anonymity API before accepting it.
Test cases
| Input | Strong pattern result | Reason |
|---|---|---|
Password1! | Match | All four classes present, length 10 |
P@ssword1 | Match | @ is the special character |
password1! | No match | No uppercase |
PASSWORD1! | No match | No lowercase |
Password! | No match | No digit |
Password11 | No match | No special character |
Pa1! | No match | Too short (length 4) |
Correct Horse 1! | Match | Spaces allowed by . |
Über stark 1! | Match | Unicode allowed by .; Ü counts as uppercase |
FAQ
Where the password gets stored
The regex enforces the password format at input time. The password also has to be stored safely. For application-specific guides:
- How to Change a WordPress Password covers the four reliable methods (admin dashboard, WP-CLI, direct database edit, lost-password email) and the bcrypt hash format WordPress 6.8+ uses.
- How to Reset a Forgotten MySQL Root Password covers
--skip-grant-tables,--init-file, and the auth-plugin changes that landed in MySQL 8.4.
See also
- Regex Lookaheads and Lookbehinds: the zero-width-assertion technique that makes "must contain a digit AND an uppercase letter" work in a single pattern
- How to Match Numbers with Regex: the cousin pattern for purely numeric inputs (PINs, OTPs)
- How to Match an Email Address with Regex: the other field on every signup form
- Regex Anchors: why
^and$are non-negotiable for password validation - Regex Word Boundaries: occasionally useful for "must contain a complete word"-style password rules
- Regex Capturing Groups and Backreferences: the
(.)\1{2}trick for rejecting repeated characters - Regex Cheat Sheet: the wider syntax and engine compatibility reference
External reference: NIST Special Publication 800-63B is the current authoritative guidance on password policies. It recommends length over complexity, breach checks, and against forced periodic resets.





