Lookaround assertions
Lookahead and lookbehind are zero-width assertions: they check whether a pattern matches just after (lookahead) or just before (lookbehind) the current position without consuming any characters. They let you match a position defined by its context — for example, a digit that is followed by a currency symbol, or a word not preceded by a backslash. This reference gives the syntax for each of the four forms across PCRE, JavaScript, Python and .NET.
How it works
There are four assertions, built from two axes — direction and polarity:
(?=...) positive lookahead — next text DOES match
(?!...) negative lookahead — next text does NOT match
(?<=...) positive lookbehind — preceding text DOES match
(?<!...) negative lookbehind — preceding text does NOT match
Because they are zero-width, the engine evaluates the inner pattern, records
success or failure, then resumes at the original position. A classic use is
splitting on context without consuming a delimiter, e.g. inserting thousands
separators with (?<=\d)(?=(\d{3})+$).
Syntax table by engine
| Assertion | PCRE | JavaScript | Python re | .NET |
|---|---|---|---|---|
| Positive lookahead | (?=...) | (?=...) | (?=...) | (?=...) |
| Negative lookahead | (?!...) | (?!...) | (?!...) | (?!...) |
| Positive lookbehind | (?<=...) | (?<=...) | (?<=...) | (?<=...) |
| Negative lookbehind | (?<!...) | (?<!...) | (?<!...) | (?<!...) |
The four tokens are identical in all four engines — the difference is in what widths of lookbehind each one accepts.
Variable-length lookbehind support
This is the most common portability problem. Some engines require the lookbehind pattern to have a fixed, knowable length.
| Engine | Variable-length lookbehind |
|---|---|
| JavaScript (ES2018+) | Allowed — (?<=\d+) works |
| .NET | Allowed |
| PCRE2 | Allowed (PCRE1 required fixed width) |
Python re | Fixed-width only — (?<=\d) yes, (?<=\d+) no |
Python regex module | Allowed |
If you need (?<=\w+) or similar and are targeting Python’s built-in re, either rewrite as a capture group or switch to the third-party regex module.
Worked examples
1. Match a price number without the currency symbol
Pattern: (?<=\$)\d+(\.\d{2})?
This matches the digits in $12.99 without including the $ in the match result. The lookbehind asserts the dollar sign is present; the match is the number only.
2. Skip a word that follows “not”
Pattern: (?<!not )\bfast\b
Matches fast in “run fast” but not in “not fast”. The negative lookbehind checks what comes before the current position.
3. Password must contain a digit and a lowercase letter
Pattern: ^(?=.*\d)(?=.*[a-z]).+$
Two positive lookaheads are chained at the start anchor. Each one tests independently from position zero, so both must succeed for the overall match to succeed. This is the standard multi-condition password-policy idiom.
Tips and notes
- Lookbehind width is the main portability trap: keep it fixed-width unless you
know the engine (.NET, JS, PCRE2, Python
regex) allows variable width. - Lookarounds do not capture by default; add inner groups only if you need the inspected text.
- Chain assertions at one position —
(?=.*\d)(?=.*[a-z])is the standard password-policy idiom. - A negative lookahead at the end, like
foo(?!bar), is the readable way to say “foo not immediately followed by bar”. - Lookarounds add backtracking cost in long strings; if performance matters on large input, benchmark against a capturing approach.