C.W.K.
Stream
Lesson 09 of 10 · published

Combining Lookahead and Lookbehind

~8 min · combining, advanced

Level 0Pattern-Curious
0 XP0/90 lessons0/15 achievements
0/100 XP to next level100 XP to go0% complete

Wrap a match with assertions on both sides

You can stack lookbehind and lookahead at the same position to assert what's on BOTH sides without consuming either. Useful for surgical extraction.

Example: extract value between fixed delimiters

"Get the text between [ and ], returning ONLY the inner text."

(?<=\[)[^\]]+(?=\])
  • (?<=\[) — assert opening bracket precedes
  • [^\]]+ — match the actual content (anything but a closing bracket)
  • (?=\]) — assert closing bracket follows

The match is just the inner content. The brackets are detected but excluded.

Same with capture group, less elegant

Without lookaround: \[([^\]]+)\] — match brackets, capture inner. Then access m.group(1). Functionally equivalent; readability cost moves between pattern and code.

When to prefer wrapped lookaround

Use wrapped lookaround when:

  • You want findall to return clean values without unpacking tuples.
  • You want the same pattern to reuse in sed or ripgrep --replace where capture groups are awkward.
  • The pattern is part of a more complex regex and you want one less capture slot to manage.

When to prefer capture groups

Use capture groups when:

  • Your engine doesn't support lookbehind (Go, RE2).
  • The lookbehind would need to be variable-width and your engine restricts.
  • You want the surrounding text accessible for replacement (e.g., wrapping with brackets in sub).

Code

Wrapped lookaround vs capture·python
import re

# Wrapped lookaround — clean values
re.findall(r'(?<=\[)[^\]]+(?=\])', '[apple] [banana] [cherry]')
# ['apple', 'banana', 'cherry']

# Capture group equivalent — tuples or extra unpacking
re.findall(r'\[([^\]]+)\]', '[apple] [banana] [cherry]')
# ['apple', 'banana', 'cherry']  — same here because findall with one group flattens

# Word between two specific markers
re.findall(r'(?<=ID:)\w+(?= accepted)', 'ID:abc123 accepted ID:def456 rejected')
# ['abc123']  — only the accepted one

# Practical: extract template variable names
# {{username}} or {{ user.name }} → 'username' or 'user.name'
re.findall(r'(?<=\{\{)\s*([\w.]+)\s*(?=\}\})', 'Hello {{ user.name }}, welcome {{username}}')
# ['user.name', 'username']  — using a capture inside lookaround

External links

Exercise

Write a single regex that finds all template placeholders like {{name}} in a string and returns the inner names without the braces. Test against Hi {{user}}, your order {{order_id}} is ready. Result: ['user', 'order_id'].

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.