Skip to main content
Regex Guide

Regex Lookaheads and Lookbehinds
Practical Guide with Examples

Assert context without consuming it — match exactly what you need, leaving the surrounding text untouched.

9 min read·Updated May 2026

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.

(?=…)
Positive lookahead
Asserts the pattern DOES follow
(?!…)
Negative lookahead
Asserts the pattern does NOT follow
(?<=…)
Positive lookbehind
Asserts the pattern DOES precede
(?
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

Variable-width patterns in lookbehinds
Most engines (JavaScript V8, Python re) require fixed-width lookbehind patterns. Writing (?<=\w+:) will throw a syntax error. Use a fixed alternative like (?<=host:) or (?<=\w{1,20}:), or switch to the Python regex module for truly variable-width lookbehinds.
Using a lookaround when a capture group is simpler
If you write \$(\d+\.\d{2}) with a capture group, you already get the number in match[1] — no lookbehind needed. Reserve lookarounds for cases where you genuinely cannot capture the surrounding context, such as when you need every match to be non-overlapping or when using findall() without capturing.
Expecting the asserted text to be in the match result
Lookarounds are zero-width — they assert but do not consume. \d+(?= dollars) matches the digits only; "dollars" never appears in match[0]. If you also need "dollars" in the result, use a regular pattern without a lookaround, or add a second capture group.
Forgetting that lookaheads chain left-to-right
You can stack multiple lookaheads at the same position: (?=.*\d)(?=.*[A-Z])\S{8,} is a common password-strength pattern. Each lookahead is evaluated from the same starting position. The order does not affect correctness but can affect readability — put the most selective assertion first.

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 →