Regex Lookaheads and Lookbehinds
Practical Guide with Examples
Assert context without consuming it — match exactly what you need, leaving the surrounding text untouched.
Regex lookaheads and lookbehinds — collectively called lookarounds — match a position rather than consuming characters, which means you can assert that a pattern is (or isn't) followed or preceded by something without including that something in the match.
The Four Lookaround Types
There are two directions (ahead / behind) and two polarities (positive / negative), giving four forms. All of them are zero-width — they never advance the match cursor.
(?=…)
(?!…)
(?<=…)
(?
Negative lookbehind
Asserts the pattern does NOT precede
Positive Lookahead (?=…)
Use a positive lookahead when you want to match something only if it is immediately followed by a specific pattern. The pattern inside the lookahead must be present, but it is never included in the match result.
// Match a number only when followed by ' dollars'
/\d+(?= dollars)/
'Pay 50 dollars now'.match(/\d+(?= dollars)/)[0]
// → '50' (' dollars' is NOT in the match)
// Match a word only when it appears at the end of a sentence
/\b\w+(?=[.!?])/g
'Stop. Look. Listen!'.match(/\b\w+(?=[.!?])/g)
// → ['Stop', 'Look', 'Listen']
Negative Lookahead (?!…)
A negative lookahead matches only when the asserted pattern is not present after the current position. It is useful for excluding specific suffixes or enforcing that a match does not appear in a particular context.
// Match 'color' but NOT 'colorful'
/color(?!ful)/g
'I like color, but colorful is better'.match(/color(?!ful)/g)
// → ['color'] (the second 'color' inside 'colorful' is excluded)
// Password rule: must NOT be immediately followed by a space
/^\S+(?! )/
// Fails for 'pass word', passes for 'passw0rd'
// Exclude file extensions from a match
/\bapp(?!\.min\.js)\b/g
// Matches 'app.js' and 'app' but not 'app.min.js'
Positive Lookbehind (?<=…)
A positive lookbehind asserts that a pattern must immediately precede the match. This lets you extract a value that only makes sense in a specific context — like a number that follows a currency symbol.
// Match a price number only when preceded by '$'
/(?<=\$)\d+(\.\d{2})?/g
'Total: $19.99, was $24.00'.match(/(?<=\$)\d+(\.\d{2})?/g)
// → ['19.99', '24.00'] ($ is NOT in the results)
// Extract the value that follows a label like 'Version: '
/(?<=Version:\s)\S+/
'Version: 3.14.1'.match(/(?<=Version:\s)\S+/)[0]
// → '3.14.1'
// Extract query param value after 'id='
/(?<=[?&]id=)[^&]+/
'https://example.com/page?id=42&ref=home'.match(/(?<=[?&]id=)[^&]+/)[0]
// → '42'
Negative Lookbehind (?<!…)
A negative lookbehind matches only when the asserted pattern is not immediately before the current position. A common use is matching a standalone word that must not appear as part of a hyphenated compound.
// Match 'line' only when NOT preceded by a hyphen (not 'on-line')
/(?<!-)\bline\b/g
'line on-line airline'.match(/(?<!-)\bline\b/g)
// → ['line']
// Match a number that is NOT preceded by a minus sign
/(?<!-)\b\d+/g
'temp: -5, height: 180, offset: -20'.match(/(?<!-)\b\d+/g)
// → ['180']
// Avoid double-encoding — skip '&' already followed by 'amp;'
/&(?!amp;)/g // (this one is a negative lookahead, shown for contrast)
JavaScript Examples with matchAll()
When you need every match in a string together with its surrounding context, combine lookarounds with matchAll() and named groups.
// Extract all prices from a receipt string
const receipt = 'Coffee $3.50, Cake $4.75, Water $1.20';
const priceRe = /(?<=\$)(?<amount>\d+\.\d{2})/g;
for (const m of receipt.matchAll(priceRe)) {
console.log(m.groups.amount);
}
// 3.50
// 4.75
// 1.20
// Match words that are followed by a colon (config keys)
const config = 'host: localhost\nport: 5432\nname: mydb';
const keys = [...config.matchAll(/(?<key>\w+)(?=:)/g)]
.map(m => m.groups.key);
// → ['host', 'port', 'name']
Python Examples with re.findall()
Python's re module supports all four lookaround types. Remember that lookbehinds must be fixed-width — use the third-party regex module if you need variable-width assertions.
import re
# Extract numbers preceded by '$'
text = 'Prices: $12.99, $5.00, $149.95'
prices = re.findall(r'(?<=\$)\d+\.\d{2}', text)
# → ['12.99', '5.00', '149.95']
# Match words followed by a period (sentence-ending words)
sentence = 'Run. Jump. Stop now.'
words = re.findall(r'\b\w+(?=\.)', sentence)
# → ['Run', 'Jump', 'now']
# Negative lookbehind: numbers NOT after a minus sign
data = 'Values: -3, 42, -7, 100'
positives = re.findall(r'(?<!-)\b\d+', data)
# → ['42', '100']
# Use re.finditer for match objects (gives position info)
for m in re.finditer(r'(?<=port:\s)\d+', 'port: 8080 port: 443'):
print(m.group(), 'at', m.start())
# 8080 at 6
# 443 at 17
Common Mistakes
Test Your Lookarounds Live
Paste any pattern from this page into the Regex Tester — highlights update in real time so you can see exactly what the lookaround is asserting without including in the match.
Open Regex Tester →