Lookahead and lookbehind

Regular expressions · Advanced · 12 min

Lookarounds express conditions like 'a number not followed by %' or 'must contain a digit' that plain patterns can't.

Lookarounds are zero-width assertions, like ^ and \b, but with a sub-pattern you choose. They check a condition at the current position and consume nothing.

  • X(?=Y) positive lookahead: X only if followed by Y.
  • X(?!Y) negative lookahead: X only if not followed by Y.
  • (?<=Y)X positive lookbehind: X only if preceded by Y.
  • (?<!Y)X negative lookbehind: X only if not preceded by Y.

Typical uses:

  • Extract a value without its label: (?<=price: )\d+.
  • Exclude a context: \b\d+\b(?!%) numbers that aren't percentages.
  • Several independent conditions at one position: ^(?=.*\d)(?=.*[a-z]).{8,}$ requires a digit and a lowercase letter, in any order, in a string of at least 8 characters.
  • Insert at a position: re.sub(r"\B(?=(\d{3})+$)", ",", "1234567") adds thousands separators.

Support differs. Python and PCRE require lookbehind to have a fixed length (PCRE2 allows alternatives of different fixed lengths, and 10.43+ limited variable length); JavaScript and .NET allow any length. Go and Rust's standard engines have no lookarounds at all.

Example

import re

print(re.findall(r"(?<=price: )\d+", "price: 40, tax: 8"))       # ['40']
print(re.findall(r"\b\d+\b(?!%)", "5 apples, 20% off, 3 left"))  # ['5', '3']
print(re.findall(r"(?<!-)\b\d+", "a 5, b -3"))                   # ['5']

strong = re.compile(r"^(?=.*\d)(?=.*[a-z]).{8,}$")
print(bool(strong.match("password")))     # False: no digit
print(bool(strong.match("passw0rd")))     # True

print(re.sub(r"\B(?=(\d{3})+$)", ",", "1234567"))   # 1,234,567

try:
    re.compile(r"(?<=a+)b")
except re.error as e:
    print("error:", e)    # error: look-behind requires fixed-width pattern

Remember

  • Lookarounds assert a condition without consuming text
  • Chain lookaheads at ^ to require several things in any order
  • Lookbehind must be fixed-width in Python and most PCRE versions; Go and Rust have none

Watch out for

  • Expecting the lookahead's text to be part of the match or of a replaced span; it is only checked.

Check yourself

  1. What does re.findall(r'\w+(?=:)', 'host: a, port: 80') return?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions/Assertions

Builds on

Unlocks

Nothing builds on this yet.

Lookahead and lookbehind · Regular expressions · Topolearn