Regex Lookahead & Lookbehind: Positive and Negative Assertions Explained
Lookahead and lookbehind are among the most powerful — and most misunderstood — features in regular expressions. They let you match text based on what surrounds it, without including that surrounding context in your match. This guide explains all four lookaround assertion types: positive lookahead, negative lookahead, positive lookbehind, and negative lookbehind — with clear examples in JavaScript and Python.
What Are Lookaround Assertions?
Lookaround assertions are zero-width assertions — they check whether a pattern matches at a position, but they don't consume any characters. The match position stays exactly where it was after the assertion succeeds. This makes them ideal for matching content that's adjacent to specific text without capturing that text in the result.
There are four types:
| Type | Syntax | Meaning |
|---|---|---|
| Positive lookahead | (?=...) | Match if followed by pattern |
| Negative lookahead | (?!...) | Match if NOT followed by pattern |
| Positive lookbehind | (?<=...) | Match if preceded by pattern |
| Negative lookbehind | (?<!...) | Match if NOT preceded by pattern |
The memory aid: ahead = what comes after (think: looking forward), behind = what came before (think: looking back). The =means "it is there" (positive), the ! means "it is NOT there" (negative).
Positive Lookahead (?=...)
A positive lookahead matches the current position only if the pattern inside the lookahead matches what follows — but the lookahead text is not included in the match result.
// Positive lookahead: (?=...)
// Match "foo" only if followed by "bar"
const regex = /foo(?=bar)/;
regex.test("foobar"); // true — "foo" is followed by "bar"
regex.test("foobaz"); // false — "foo" is not followed by "bar"
regex.test("foo"); // false — nothing after "foo"
// Extract prices: match digits only when followed by a dollar sign
const priceRegex = /\d+(?=\$)/g;
"100$ and 200€ and 50$".match(priceRegex);
// → ["100", "50"] (200 not matched — not followed by $)Notice the key behavior: in the prices example, the matched text is just the digits — not the dollar sign. The $ is checked (the lookahead asserts it must be there) but not consumed. This is what "zero-width" means.
Negative Lookahead (?!...)
A negative lookahead matches the current position only if the pattern inside the lookahead does not match what follows. It's the inverse of positive lookahead and extremely useful for exclusion patterns.
// Negative lookahead: (?!...)
// Match "foo" only if NOT followed by "bar"
const regex = /foo(?!bar)/;
regex.test("foobar"); // false — "foo" IS followed by "bar"
regex.test("foobaz"); // true — "foo" is not followed by "bar"
regex.test("foo"); // true — nothing after "foo" (not "bar")
// Match file names that are NOT .log files
const notLogRegex = /\w+(?!\.log)\.[a-z]+/g;
// More common pattern: password must NOT contain spaces
const noSpaceRegex = /^(?!.*\s).{8,}$/;
noSpaceRegex.test("secure123"); // true
noSpaceRegex.test("has spaces"); // false💡 Common use: password validation
Negative lookahead is perfect for "must NOT contain" requirements. The pattern(?!.*\s) asserts that nowhere in the string does a whitespace character appear — without needing to write a complex alternative.
Positive Lookbehind (?<=...)
A positive lookbehind matches the current position only if the pattern inside the lookbehind matches what immediately precedes it. Like lookahead, the lookbehind text is not included in the match.
// Positive lookbehind: (?<=...) // Match digits only when preceded by a dollar sign const regex = /(?<=\$)\d+/g; "$100 and €200 and $50".match(regex); // → ["100", "50"] (200 not matched — preceded by € not $) // Match a word only when preceded by "Mr." or "Ms." const nameRegex = /(?<=M[rs]\. )\w+/g; "Mr. Smith and Ms. Jones attended.".match(nameRegex); // → ["Smith", "Jones"] // Extract version numbers after "v" const versionRegex = /(?<=v)\d+\.\d+\.\d+/g; "Release v2.1.0 and v3.0.0-beta".match(versionRegex); // → ["2.1.0", "3.0.0"]
Positive lookbehind is the clean way to extract values that always follow a specific prefix — currency amounts, version numbers, key-value pairs — without having to use a capture group and extract group 1.
Negative Lookbehind (?<!...)
A negative lookbehind matches the current position only if the pattern inside the lookbehind does not match what precedes it. Use it to exclude matches that follow a specific prefix.
// Negative lookbehind: (?<!...)
// Match digits NOT preceded by a dollar sign
const regex = /(?<!\$)\d+/g;
"$100 and 200 items and $50".match(regex);
// → ["200"] (100 and 50 are preceded by $ so excluded)
// Match "port" only when not preceded by "trans"
const portRegex = /(?<!trans)port/g;
"import export transport port".match(portRegex);
// → ["port"] in "import", "export", and standalone "port"
// Note: "transport" is excluded
// Avoid matching CSS hex colors that aren't 6-digit
const hexRegex = /(?<!#[0-9a-fA-F]{1,5})\b[0-9a-fA-F]{6}\b/g;Combining Lookahead and Lookbehind
The real power emerges when you combine multiple lookaround assertions. Password validation is the canonical example — expressing multiple independent requirements in a single pattern using chained positive and negative lookaheads:
// Combining lookahead and lookbehind in one pattern
// Match content between quotes but don't include the quotes
const betweenQuotes = /(?<=")[^"]+(?=")/g;
'"hello" and "world"'.match(betweenQuotes);
// → ["hello", "world"]
// Password validation: at least 8 chars, one uppercase,
// one digit, no spaces — all using lookaheads
const passwordRegex = /^(?=.*[A-Z])(?=.*\d)(?!.*\s).{8,}$/;
passwordRegex.test("Secure123"); // true
passwordRegex.test("nouppercase1"); // false — no uppercase
passwordRegex.test("NoDigits!"); // false — no digit
passwordRegex.test("Has Space1"); // false — has space
// Extract CSS property values (positive lookbehind + lookahead)
const cssValueRegex = /(?<=color:\s*)[^;]+(?=;)/g;
"color: #ff0000; background: blue;".match(cssValueRegex);
// → ["#ff0000"]Lookahead & Lookbehind in Python
Python's re module supports all four lookaround assertion types. The syntax is identical to JavaScript. Variable-length lookbehind is also supported in Python (unlike some engines that require fixed-length lookbehind):
import re
# Python supports all four lookaround types in the re module
# Positive lookahead: match word followed by a colon
pattern = re.compile(r'\w+(?=:)')
pattern.findall("name: Alice age: 30 city: NYC")
# → ['name', 'age', 'city']
# Negative lookahead: match .py files but not __init__.py
pattern = re.compile(r'(?!__init__)\w+\.py')
files = ["app.py", "__init__.py", "utils.py"]
[f for f in files if pattern.match(f)]
# → ['app.py', 'utils.py']
# Positive lookbehind: extract values after "="
pattern = re.compile(r'(?<==)[\w.]+')
pattern.findall("host=localhost port=5432 db=mydb")
# → ['localhost', '5432', 'mydb']
# Negative lookbehind: match numbers not part of a float
pattern = re.compile(r'(?<!\.)\b\d+\b(?!\.)')
pattern.findall("100 3.14 42 0.5 7")
# → ['100', '42', '7']Language & Engine Support
Lookaround assertions are widely supported but not universal. Here's what to expect across environments:
# Browser JavaScript: all four types supported (ES2018+) /(?<=prefix)\w+/ # positive lookbehind ✅ /(?<!prefix)\w+/ # negative lookbehind ✅ /\w+(?=suffix)/ # positive lookahead ✅ /\w+(?!suffix)/ # negative lookahead ✅ # Python re module: all four supported import re re.findall(r'(?<=@)\w+', 'user@example.com') # → ['example'] # Java (java.util.regex): all four supported # .NET: all four supported (variable-length lookbehind too) # Go (regexp package): lookaheads/lookbehinds NOT supported # Use RE2 syntax instead; consider alternative approaches # POSIX ERE (grep -E, sed): NO lookaround support # Use Perl-compatible regex (grep -P) for lookaround in shell
⚠️ Variable-length lookbehind
Some engines (older JavaScript, Ruby) require fixed-length lookbehind — meaning the pattern inside (?<=...) must match a fixed number of characters. Python and .NET support variable-length lookbehind. Modern JavaScript (V8, SpiderMonkey) supports variable-length lookbehind since ES2018.
Performance Considerations
Lookaround assertions can impact performance on large inputs because the regex engine may need to check the assertion at every position. For most text-processing tasks this is negligible. Two things to watch for:
- Avoid nested quantifiers inside lookaheads — patterns like
(?=.*pattern)applied to a long string force backtracking at every position. Consider anchoring with^when validating entire strings. - Complex combined lookaheads on large files — if you're processing gigabytes of text, test performance and consider splitting the logic into multiple passes.
Quick Reference
(?=abc)— Positive lookahead: must be followed by "abc"(?!abc)— Negative lookahead: must NOT be followed by "abc"(?<=abc)— Positive lookbehind: must be preceded by "abc"(?<!abc)— Negative lookbehind: must NOT be preceded by "abc"
If you find yourself struggling to construct the right lookaround pattern, describe what you want in plain English and let an AI regex generator build it for you — lookaheads and lookbehinds included.
Generate lookahead & lookbehind patterns instantly
Describe your regex requirement in plain English — "match prices preceded by $", "passwords without spaces", "words not followed by a comma" — and RegSQL generates the correct pattern with an explanation.
✨ Try RegSQL Regex Generator Free →Summary
Lookahead and lookbehind assertions give you positional matching without consuming characters — letting you write precise patterns that would otherwise require capture groups or post-processing. Master the four types (positive/negative lookahead, positive/negative lookbehind) and you unlock a whole class of clean, powerful regex solutions.