Regular Expressions › Lookahead and Lookbehind

Michael BrenndoerferUpdated March 18, 20253 min read
Language AI HandbookAll chapters
Part 1 of 67Text as Data
Chapter 3Regular Expressions › Lookahead and Lookbehind
Reading section 9 of 21 · Lookahead and Lookbehind · 3 minSection 9 of 21 · 3 min

Book contents

Section 9 of 21 · 3 min read

Regular Expressions › Lookahead and LookbehindOne section · Sections in this chapter9 / 21 ↓
Reading tools Definitions & inline links

Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.

Article links

Make inline references clickable

Lookahead and lookbehind assertions match a position based on what comes before or after, without consuming any characters. These are called "zero-width assertions" because they check conditions without advancing the regex engine's position in the string. This makes them powerful for extracting text that appears in specific contexts while excluding the context itself from the match.

Think of lookarounds as asking the question "is the text around my target in the right form?" before returning the target. A positive lookahead (?=...) asks "is this text followed by ...?" A negative lookahead (?!...) asks "is this text NOT followed by ...?" The lookbehind variants ask the same questions about what precedes the target. The key detail is that the lookaround's content is checked but not consumed: it does not become part of the match and does not move the engine forward.

Lookahead

In[37]:
Code
text = "100 dollars, 50 euros, 75 pounds"

# Positive lookahead: (?=...)
# Match numbers followed by "dollars"
dollars = re.findall(r"\d+(?= dollars)", text)

# Negative lookahead: (?!...)
# Match numbers NOT followed by "dollars"
not_dollars = re.findall(r"\d+(?! dollars)", text)
Out[38]:
Console
Text: '100 dollars, 50 euros, 75 pounds'

Positive lookahead (?= dollars):
  Pattern: \d+(?= dollars)
  Matches: ['100']

Negative lookahead (?! dollars):
  Pattern: \d+(?! dollars)
  Matches: ['10', '50', '75']

Lookbehind

In[39]:
Code
text = "$100 €50 £75"

# Positive lookbehind: (?<=...)
# Match numbers preceded by $
usd = re.findall(r"(?<=\$)\d+", text)

# Negative lookbehind: (?<!...)
# Match numbers NOT preceded by $
not_usd = re.findall(r"(?<!\$)\d+", text)
Out[40]:
Console
Text: '$100 €50 £75'

Positive lookbehind (?<=\$):
  Pattern: (?<=\$)\d+
  Matches: ['100']

Negative lookbehind (?<!\$):
  Pattern: (?<!\$)\d+
  Matches: ['00', '50', '75']

Lookarounds are powerful for extracting data from structured formats where you want the context to guide matching but don't want the context in your result.

The practical difference between a lookbehind and a capturing group often comes down to whether you need the context to appear in your output. If you write (?<=price=)\d+, only the number is captured. If you write price=(\d+), you get the number in group 1 but you have to be careful about how you use findall() (it will return group contents rather than full matches). Lookarounds are cleaner when the context is structural scaffolding that you want to use as a filter but never include in results.

Python's re module requires that lookbehind patterns have fixed width: (?<=\d{3}) is allowed because \d{3} always consumes exactly three characters, but (?<=\d+) raises an error because \d+ can match any number of characters. The third-party regex module lifts this restriction if you need variable-width lookbehinds.

In[41]:
Code
# Practical example: Extract values from key-value pairs
config = "name=Alice, age=30, city=Boston"

# Use lookbehind to find values after specific keys
name = re.search(r"(?<=name=)\w+", config)
age = re.search(r"(?<=age=)\d+", config)
Out[42]:
Console
Config: 'name=Alice, age=30, city=Boston'

Extracting with lookbehind:
  name: Alice
  age: 30