Lookahead and lookbehind

re · Advanced · 11 min

Lookarounds express conditions like 'a number followed by px' or 'a password containing a digit' cleanly.

Lookarounds are zero-width assertions: they check a condition at the current position but don't include the text in the match.

  • (?=...) positive lookahead: what follows must match.
  • (?!...) negative lookahead: what follows must not match.
  • (?<=...) positive lookbehind: what precedes must match.
  • (?<!...) negative lookbehind: what precedes must not match.

Examples:

  • r"\d+(?=px)" matches the number in 12px but not the unit.
  • r"(?<=\$)\d+" matches amounts after a dollar sign.
  • r"\b(?!test_)\w+\.py\b" matches Python files not starting with test_.
  • Stacked lookaheads at the start test several conditions independently: r"^(?=.*\d)(?=.*[A-Z]).{8,}$" requires a digit, an uppercase letter and length 8+.

Python requires lookbehind to be fixed width: (?<=\$|USD ) fails because the alternatives differ in length (write (?:(?<=\$)|(?<=USD )) instead), and quantifiers like + aren't allowed inside. The third-party regex module lifts that restriction.

Lookarounds are great in re.sub, because they let you insert text at a position: re.sub(r"(?<=\d)(?=(\d{3})+$)", ",", "1234567") adds thousands separators.

Readability suffers quickly; when a condition is easier as a second check in Python, write the second check.

Example

import re

print(re.findall(r"\d+(?=px)", "w: 12px; h: 30em; m: 4px"))   # ['12', '4']
print(re.findall(r"(?<=\$)\d+", "cost $40, tax 5, tip $6"))   # ['40', '6']
print(re.findall(r"\b(?!test_)\w+\.py\b", "app.py test_app.py db.py"))
# ['app.py', 'db.py']

strong = re.compile(r"^(?=.*\d)(?=.*[A-Z]).{8,}$")
for pw in ["password1", "Password1", "Pass1"]:
    print(pw, bool(strong.match(pw)))
# password1 False
# Password1 True
# Pass1 False

print(re.sub(r"(?<=\d)(?=(?:\d{3})+$)", ",", "1234567"))  # 1,234,567
try:
    re.compile(r"(?<=\$|USD )\d+")
except re.error as e:
    print(e)        # look-behind requires fixed-width pattern

Remember

  • Lookarounds assert context without consuming characters
  • Python lookbehind must be fixed width
  • Stacked lookaheads test independent conditions at one position

Watch out for

  • Writing a variable-width lookbehind such as (?<=\s+), which raises re.error.

Check yourself

  1. What does re.sub(r'(?<=a)b', 'X', 'ab cb ab') return?

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

Official docs: docs.python.org/3/library/re.html#regular-expression-syntax