Skip to content

Chapter 5 of 8

Lookaround Assertions

Lookarounds are zero-width assertions that test what is ahead or behind the current position without consuming characters. Positive lookahead, written (?=...), asserts that the upcoming text matches the subpattern and then returns the position to where it was, so the surrounding pattern can continue matching. The pattern \d+(?=px) captures the digits in 42px but rejects 42em because the lookahead requires the digits to be followed by px. Negative lookahead, written (?!...), asserts that the upcoming text does NOT match the subpattern, so foo(?!bar) matches foo only when it is not immediately followed by bar. Lookaheads are evaluated at the current position, and the engine does not advance past them, which is why they are perfect for prefix and suffix constraints.

Lookbehinds are the mirror image: they inspect the text before the current position. Positive lookbehind, written (?<=...), asserts that what precedes the position matches the subpattern; negative lookbehind, written (?<!...), asserts the opposite. A practical use is matching a price without including the currency symbol: (?<=\$)\d+(\.\d{2})? matches 19.99 in $19.99 but only when a dollar sign precedes it. Lookbehinds were historically limited to fixed-width subpatterns, which constrained their usefulness, but modern engines have relaxed this. JavaScript supports lookbehinds since ES2018, Python 3.11+ allows variable-width lookbehinds, and PCRE2 supports them fully. When you need a feature that is not yet universal, prefer lookahead, which is widely supported across all flavors.

Lookarounds become particularly powerful when you need to enforce several independent rules in a single pattern. Password validation is the classic example: ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d!@#\(%^&*]{8,}\) uses three positive lookaheads to require at least one lowercase letter, one uppercase letter, and one digit, while the final consuming class with the length quantifier ensures the entire input is at least eight characters and made of allowed symbols. The lookaheads do not consume characters, so each one independently scans the string from the start. This composition is much harder to express with capturing groups or alternation. Lookarounds also make it easy to convert re.match semantics to re.search semantics in Python, because the leading ^ can be replaced with a lookahead that does not consume the start of input.

All chapters
  1. 1Character Matching: Metacharacters, Shorthands, and Classes
  2. 2Quantifiers: Greedy, Lazy, Possessive, and Exact
  3. 3Anchors and Boundaries
  4. 4Groups, Captures, and Backreferences
  5. 5Lookaround Assertions
  6. 6Flags, Modes, and Engine Features
  7. 7Practical Recipes for Validation and Extraction
  8. 8Performance and Engine Internals

Drill it

Reading is not remembering. These come from the Regex Patterns Every Developer Should Know deck:

Q

What does the regex metacharacter <code>.</code> match by default?

A single character of any kind except the newline character (\n). To make it also match newlines, use the s (dotall / single-line) flag, e.g. /foo.bar/s in Java...

Q

Which character class matches any single digit in most regex flavors?

[0-9] or the shorthand \d. \d is equivalent to [0-9] in ASCII; with Unicode (u flag in JS, default in Python 3 str) it matches any Unicode decimal digit, e.g. A...

Q

What does the shorthand <code>\w</code> match?

Any word character. In ASCII mode this is [A-Za-z0-9_]; in Unicode mode it also matches letters and digits from other scripts, and the underscore.

Q

What does the shorthand <code>\s</code> match?

Any whitespace character: space, tab (\t), newline (\n), carriage return (\r), form feed (\f), and vertical tab (\v). In Unicode mode it can also match rare whi...