The password regex you can copy
This is the classic "strong password" pattern built entirely from lookaheads:
^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,64}$
It demands four things at once — an uppercase letter, a lowercase letter, a digit, and a symbol — and a total length between 8 and 64. The trick is that none of those four requirements is written as ordinary consumption; they are all checks that peek ahead and then let the real match happen at the end.
Why lookaheads are the right tool
A normal token in a regex consumes characters and advances the cursor. If you wrote [A-Z] at the start, the pattern would demand the first character be uppercase, which is far stricter than "has at least one uppercase somewhere." Lookaheads fix this.
(?=...)is a zero-width positive lookahead. It asserts that, starting at the current position, the sub-pattern can match, but it does not move the cursor or add to the match..*[A-Z]inside the lookahead means "anywhere ahead of here there is an uppercase letter." The.*does the scanning; the[A-Z]is the thing we are hunting for.- Because each lookahead returns to the same starting point, you can stack four of them and they all scan the whole string independently.
Only after all four assertions pass does .{8,64}$ actually consume the password and confirm the length. The cursor never moved during the checks; that is the whole point.
Why NIST tells you not to do this
This is the core argument. NIST Special Publication 800-63B, the document most banks and governments defer to, explicitly argues against mandated character composition. The reasoning:
- Forced mixtures produce passwords like
Password1!that satisfy the rule while being among the first an attacker tries. They raise user frustration without raising real entropy. - They cap creativity: a long passphrase of random words (
correct-horse-battery-staplestyle) is rejected for lacking a digit, even though it is vastly stronger thanP@ssw0rd. - NIST instead recommends a generous minimum length (8 is the floor, 12+ is better), a check against known breached passwords, and suppression of obvious patterns.
The regex above is a fine teaching example. As a production policy it is fighting the wrong battle.
Performance and the bugs people actually hit
Lookahead patterns are usually fast because the string is scanned a small constant number of times. The failures are subtler:
- Forgetting the anchor: writing
(?=.*[A-Z])without a leading^lets the check start mid-string and can green-light a password that only meets rules after junk. - Dropping the
$: without$after.{8,64}the engine may accept a 200-character password because it matched the first 64 and stopped caring. - OR instead of AND: if you mistakenly join rules with
|instead of stacking lookaheads, the password only needs to satisfy one branch, defeating the policy. - Newline surprise:
.*does not cross line breaks unless thes(dotall) flag is set. A pasted password with an embedded newline can slip past a rule unexpectedly.
Common wrong patterns and the fix
| Wrong pattern | Problem | Fix |
|---|---|---|
| ^(?=.*[A-Z])$ | Only checks one rule, accepts anything else | Stack all lookaheads before .{n,m}$ |
| ^(?=.*[A-Z])(?=.*[a-z]).{8,}$ | No digit or symbol requirement | Add (?=.*\d) and (?=.*[!@#$%^&*]) |
| ^[A-Za-z\d!@#]{8,}$ | Requires mixed classes only at position 1 | Use lookaheads so classes can appear anywhere |
| ^.{8,64}$ | No composition at all (sometimes fine!) | Accept if you follow NIST length-first advice |
Strict and loose variants
You can tighten the symbol set or relax it. A stricter rule might require two symbols; a looser one might accept any non-alphanumeric via \W. Be aware that \W includes whitespace, so a trailing space would count as a "symbol" — usually not what you want. The length-first variant ^.{12,64}$ is the most defensible modern choice and the easiest for users to satisfy.
What to use instead
Move the real security work out of the regex:
- Length first: enforce a minimum (12 is a good bar) and a sane maximum to block denial-of-service via huge inputs.
- Breach check: reject the password if it appears in a leaked-password dataset such as the Have I Been Pwned range API.
- Entropy estimate: for a strength meter, compute Shannon entropy or use a zxcvbn-style estimator that understands dictionary words and keyboard patterns.
A regex can still render the UI hint, but the actual gate should be length plus a breach lookup, not a character-class obstacle course.
Code: checking a breach list
import hashlib, requests
def is_breached(pw):
h = hashlib.sha1(pw.encode()).hexdigest().upper()
prefix, tail = h[:5], h[5:]
resp = requests.get("https://api.pwnedpasswords.com/range/" + prefix)
return tail in resp.text
Pair that with a simple length check and you have a policy that is both safer and less annoying than the four-lookahead monster.