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:Xonly if followed byY.X(?!Y)negative lookahead:Xonly if not followed byY.(?<=Y)Xpositive lookbehind:Xonly if preceded byY.(?<!Y)Xnegative lookbehind:Xonly if not preceded byY.
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 patternRemember
- 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.