Lookahead
Regex Lookahead — The Complete Guide
Master lookahead and lookbehind assertions to match text based on what comes before or after — without consuming it.
Introduction
A lookahead is a regex assertion that checks whether a pattern is (or is not) followed by another pattern — without including that other pattern in the match. This "zero-width" behaviour makes lookaheads incredibly powerful for validating context, enforcing rules (like password requirements), and matching text based on its surroundings rather than just its own characters.
Real-World Analogy
The Bouncer's Glance
A club bouncer glances ahead to check if you're on the guest list before letting you through — but that glance doesn't move you forward; you still walk in yourself. A lookahead is that glance: it verifies what's ahead to allow the match, without stepping over any characters.
Prerequisites
Before You Start
- Python 3.x installed
- Understanding of the
remodule (search, findall, sub) - Familiarity with groups and common tokens (
\d,\w,.) - Comfort using raw strings
r"..."for patterns - Awareness of what "consuming" characters means in regex
The Key Idea: Zero-Width
Normal tokens consume characters — they move the match position forward. Lookarounds are zero-width: they test a condition at the current position but do not advance or include anything in the result.
The Four Lookaround Assertions
Positive Lookahead (?=...)
Match only if followed by a pattern.
X(?=Y) matches X only when it is immediately followed by Y. Y is not part of the match.
Negative Lookahead (?!...)
Match only if NOT followed by a pattern.
X(?!Y) matches X only when it is not immediately followed by Y.
Positive Lookbehind (?<=...)
Match only if preceded by a pattern.
(?<=Y)X matches X only when it is immediately preceded by Y.
Negative Lookbehind (?<!...)
Match only if NOT preceded by a pattern.
(?<!Y)X matches X only when it is not preceded by Y.
Syntax Reference
| Syntax | Name | Matches X when... |
|---|---|---|
X(?=Y) |
Positive lookahead | X is followed by Y |
X(?!Y) |
Negative lookahead | X is not followed by Y |
(?<=Y)X |
Positive lookbehind | X is preceded by Y |
(?<!Y)X |
Negative lookbehind | X is not preceded by Y |
Positive Lookahead (?=...)
Match a number only if it is followed by "USD".
import re
text = "100USD 200EUR 300USD"
# \d+ that is followed by "USD" — but "USD" is NOT captured
matches = re.findall(r"\d+(?=USD)", text)
print(matches) # ['100', '300']
Negative Lookahead (?!...)
Match a number only if it is NOT followed by "USD".
import re
text = "100USD 200EUR 300USD"
matches = re.findall(r"\d+(?!USD)", text)
print(matches) # ['200'] and partial digits — see note below
\b or word boundaries.
import re
# Safer: whole numbers not followed by USD
text = "100USD 200EUR 300USD"
matches = re.findall(r"\b\d+\b(?!USD)", text)
print(matches) # ['200']
Lookbehind (?<=...) and (?<!...)
Match text based on what comes before it.
import re
text = "$100 and €200 and $300"
# Positive lookbehind: numbers preceded by "$"
dollars = re.findall(r"(?<=\$)\d+", text)
print(dollars) # ['100', '300']
# Negative lookbehind: numbers NOT preceded by "$"
non_dollars = re.findall(r"(?<!\$)\b\d+", text)
print(non_dollars) # ['200']
re, lookbehind must be fixed-width — (?<=abc) is fine, but (?<=a+) raises an error. Use the third-party regex module for variable-width lookbehind.
Classic Use Case: Password Validation
Multiple lookaheads stacked at the start check several independent rules at once.
import re
# Rules: at least 8 chars, one digit, one uppercase, one lowercase
pattern = r"^(?=.*\d)(?=.*[A-Z])(?=.*[a-z]).{8,}$"
def is_strong(pw):
return bool(re.match(pattern, pw))
print(is_strong("Abc12345")) # True
print(is_strong("abc12345")) # False (no uppercase)
print(is_strong("Abcdefg")) # False (no digit, too short)
(?=.*X) is zero-width, so all three checks run from the same start position before .{8,} actually consumes the string.
Practical: Thousands Separators
Lookahead can insert commas into large numbers.
import re
number = "1234567"
# Insert a comma at every position followed by groups of 3 digits
formatted = re.sub(r"\d(?=(\d{3})+$)", r"\g<0>,", number)
print(formatted) # 1,234,567
Zero-Width Formally
If a normal token consumes a substring of length \(L > 0\), a lookaround consumes \(L = 0\). So after evaluating a lookaround at position \(p\), the match position stays fixed:
\[ p_{\text{after}} = p_{\text{before}} \quad\text{(width } = 0\text{)} \]
This is exactly why you can stack several lookaheads at the same spot — none of them move the cursor.
Best Practices
Do This
- Use stacked lookaheads to validate multiple independent rules at once
- Anchor with
\b,^, or$to avoid partial matches - Keep Python lookbehind fixed-width, or switch to the
regexmodule - Remember lookarounds are zero-width — they never appear in the result
- Use lookahead in
re.subto insert without deleting surrounding text - Comment complex assertions with
re.VERBOSEfor readability
Common Mistakes
(?<=\d+) in Python's re — it raises an error.
regex module which supports variable-width.
Interview Questions
| Question | Short Answer |
|---|---|
| What is a lookahead? | A zero-width assertion that checks whether a pattern is (or isn't) followed by another. |
| Positive vs negative lookahead? | (?=...) requires the following pattern; (?!...) forbids it. |
| What does "zero-width" mean? | The assertion consumes no characters and isn't included in the match. |
| Limitation of Python lookbehind? | It must be fixed-width in the built-in re module. |
| A real use for stacked lookaheads? | Password validation — checking several rules from one position. |
Quick Revision
| Assertion | Syntax | Meaning |
|---|---|---|
| Positive lookahead | (?=...) |
Followed by |
| Negative lookahead | (?!...) |
Not followed by |
| Positive lookbehind | (?<=...) |
Preceded by |
| Negative lookbehind | (?<!...) |
Not preceded by |
Key Takeaways
Lookaheads and lookbehinds are zero-width assertions that match text by its context — what comes
before or after — without consuming it. Use (?=)/(?!) to look forward and
(?<=)/(?<!) to look back, and stack them for powerful validation like passwords.