Regex lookahead and lookbehind in practice
Most regex tutorials stop at character classes and quantifiers, and then a real-world requirement shows up — "match a password with a digit and a symbol, but don't capture them separately" — and lookaround is suddenly unavoidable. It looks scarier than it is once you separate the four forms.
The four lookaround assertions
All four are zero-width: they check that something is (or isn't) next to the current position without consuming characters, so they never appear in the matched substring.
- `(?=...)` — positive lookahead: what follows must match.
- `(?!...)` — negative lookahead: what follows must not match.
- `(?<=...)` — positive lookbehind: what precedes must match.
- `(?<!...)` — negative lookbehind: what precedes must not match.
A useful mental model: lookahead reads forward from where you are, lookbehind reads backward, and both discard what they read.
Password validation without a mess of alternation
A password that needs a lowercase letter, an uppercase letter, a digit and a symbol, at least 8 characters, is painful to express with a single character class because order isn't fixed. Lookahead solves it by stacking independent conditions at the start:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$Each `(?=.*X)` says "somewhere ahead, X exists" without moving the cursor, so the four checks stack instead of fighting for the same characters. The final `.{8,}` does the actual consuming match.
Extracting a number without its unit
Say you have `"12kg"`, `"3.5m"`, `"100px"` and you want the number but not the unit, without a capture group (useful when you're feeding a pattern into a tool that only returns the full match, like a find-and-replace field):
\d+(\.\d+)?(?=kg|m|px)The lookahead confirms a unit follows but doesn't include it in the match. Swap to `(?<=\$)\d+(\.\d+)?` and you get the amount after a dollar sign without capturing the sign itself.
Negative lookahead for "not followed by"
A classic use case is matching a word unless it's followed by a specific suffix — for example, matching `foo` but not `foobar`:
foo(?!bar)Or matching an IP-like number that isn't followed by another digit, so `192` doesn't accidentally match inside `1921`:
\b192\b(?!\d)(In this specific case a word boundary already does the job, but it's a good illustration because lookahead composes with everything else in the pattern.)
Negative lookbehind: excluding a prefix
Say you're parsing CSS and want to match a hex color that isn't preceded by `#` twice (i.e. not part of an 8-digit alpha color already matched elsewhere), or more practically: matching a currency amount not preceded by a minus sign, to treat negative and positive amounts differently:
(?<!-)\b\d+(\.\d{2})?\bLookbehind must be a fixed length in most engines (JavaScript's V8 engine now supports variable-length lookbehind, but Python's `re` module historically didn't), so if a pattern refuses to compile with a "look-behind requires fixed-width pattern" error, that's usually why. Rewriting `(?<!\d+)` as `(?<!\d)` (matching just one preceding digit) sidesteps the limitation for most cases.
Combining lookahead and lookbehind
Extracting a number that is inside parentheses without capturing the parentheses:
(?<=\()\d+(?=\))This is a common technique for pulling values out of function-call-like syntax, template placeholders like `{{value}}`, or log lines with bracketed fields, all without capture groups cluttering your match array.
Performance caveat
Lookaround assertions are cheap when they anchor near the start of a match, but a lookahead containing `.*` inside a larger alternation can cause catastrophic backtracking on adversarial input, the same way nested quantifiers do. If you're validating untrusted user input (a password field is a good example), test the pattern against a long string of repeated characters before shipping it, or use an engine with linear-time guarantees (RE2, Rust's `regex` crate) for anything facing the public internet.
Practical checklist
- Use lookahead to check conditions ahead without consuming — great for multi-rule validation.
- Use lookbehind to check what came before — great for stripping prefixes/suffixes from a match.
- Prefer fixed-width lookbehind patterns for cross-engine compatibility.
- Always test lookaround-heavy patterns against pathological input before trusting them in production.
Paste any of the patterns above into a regex tester with match highlighting and step through them against a few edge cases — seeing exactly which characters get consumed versus merely checked is the fastest way to build intuition for lookaround.