Regex lookahead and lookbehind in practice

8 min read
regex
guide

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})?\b

Lookbehind 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.

Tools from this article

← All articles