Table of Contents

    Lookahead

    PYTHON & REGULAR EXPRESSIONS

    Regex Lookahead — The Complete Guide

    Master lookahead and lookbehind assertions to match text based on what comes before or after — without consuming it.

    Introduction

    A lookahead is a regex assertion that checks whether a pattern is (or is not) followed by another pattern — without including that other pattern in the match. This "zero-width" behaviour makes lookaheads incredibly powerful for validating context, enforcing rules (like password requirements), and matching text based on its surroundings rather than just its own characters.

    In one line: A lookahead peeks at what comes next to decide a match — but never consumes those characters.

    Real-World Analogy

    The Bouncer's Glance

    A club bouncer glances ahead to check if you're on the guest list before letting you through — but that glance doesn't move you forward; you still walk in yourself. A lookahead is that glance: it verifies what's ahead to allow the match, without stepping over any characters.

    Prerequisites

    Before You Start

    • Python 3.x installed
    • Understanding of the re module (search, findall, sub)
    • Familiarity with groups and common tokens (\d, \w, .)
    • Comfort using raw strings r"..." for patterns
    • Awareness of what "consuming" characters means in regex

    The Key Idea: Zero-Width

    Normal tokens consume characters — they move the match position forward. Lookarounds are zero-width: they test a condition at the current position but do not advance or include anything in the result.

    CORE CONCEPT
    Lookarounds assert but don't consume

    The Four Lookaround Assertions

    1

    Positive Lookahead (?=...)

    Match only if followed by a pattern.

    X(?=Y) matches X only when it is immediately followed by Y. Y is not part of the match.

    2

    Negative Lookahead (?!...)

    Match only if NOT followed by a pattern.

    X(?!Y) matches X only when it is not immediately followed by Y.

    3

    Positive Lookbehind (?<=...)

    Match only if preceded by a pattern.

    (?<=Y)X matches X only when it is immediately preceded by Y.

    4

    Negative Lookbehind (?<!...)

    Match only if NOT preceded by a pattern.

    (?<!Y)X matches X only when it is not preceded by Y.

    Syntax Reference

    Syntax Name Matches X when...
    X(?=Y) Positive lookahead X is followed by Y
    X(?!Y) Negative lookahead X is not followed by Y
    (?<=Y)X Positive lookbehind X is preceded by Y
    (?<!Y)X Negative lookbehind X is not preceded by Y

    Positive Lookahead (?=...)

    Match a number only if it is followed by "USD".

    import re
    
    text = "100USD 200EUR 300USD"
    # \d+ that is followed by "USD" — but "USD" is NOT captured
    matches = re.findall(r"\d+(?=USD)", text)
    print(matches)   # ['100', '300']
    Notice The result contains only the numbers — "USD" was checked but not included in the match.

    Negative Lookahead (?!...)

    Match a number only if it is NOT followed by "USD".

    import re
    
    text = "100USD 200EUR 300USD"
    matches = re.findall(r"\d+(?!USD)", text)
    print(matches)   # ['200'] and partial digits — see note below
    Watch out Negative lookahead can produce partial matches (e.g. "10" of "100") because it succeeds at any position not followed by the pattern. Anchor carefully with \b or word boundaries.
    import re
    
    # Safer: whole numbers not followed by USD
    text = "100USD 200EUR 300USD"
    matches = re.findall(r"\b\d+\b(?!USD)", text)
    print(matches)   # ['200']

    Lookbehind (?<=...) and (?<!...)

    Match text based on what comes before it.

    import re
    
    text = "$100 and €200 and $300"
    
    # Positive lookbehind: numbers preceded by "$"
    dollars = re.findall(r"(?<=\$)\d+", text)
    print(dollars)   # ['100', '300']
    
    # Negative lookbehind: numbers NOT preceded by "$"
    non_dollars = re.findall(r"(?<!\$)\b\d+", text)
    print(non_dollars)   # ['200']
    Limitation In Python's re, lookbehind must be fixed-width(?<=abc) is fine, but (?<=a+) raises an error. Use the third-party regex module for variable-width lookbehind.

    Classic Use Case: Password Validation

    Multiple lookaheads stacked at the start check several independent rules at once.

    import re
    
    # Rules: at least 8 chars, one digit, one uppercase, one lowercase
    pattern = r"^(?=.*\d)(?=.*[A-Z])(?=.*[a-z]).{8,}$"
    
    def is_strong(pw):
        return bool(re.match(pattern, pw))
    
    print(is_strong("Abc12345"))   # True
    print(is_strong("abc12345"))   # False (no uppercase)
    print(is_strong("Abcdefg"))    # False (no digit, too short)
    Why this works Each (?=.*X) is zero-width, so all three checks run from the same start position before .{8,} actually consumes the string.

    Practical: Thousands Separators

    Lookahead can insert commas into large numbers.

    import re
    
    number = "1234567"
    # Insert a comma at every position followed by groups of 3 digits
    formatted = re.sub(r"\d(?=(\d{3})+$)", r"\g<0>,", number)
    print(formatted)   # 1,234,567

    Zero-Width Formally

    If a normal token consumes a substring of length \(L > 0\), a lookaround consumes \(L = 0\). So after evaluating a lookaround at position \(p\), the match position stays fixed:

    \[ p_{\text{after}} = p_{\text{before}} \quad\text{(width } = 0\text{)} \]

    This is exactly why you can stack several lookaheads at the same spot — none of them move the cursor.

    Best Practices

    Do This

    • Use stacked lookaheads to validate multiple independent rules at once
    • Anchor with \b, ^, or $ to avoid partial matches
    • Keep Python lookbehind fixed-width, or switch to the regex module
    • Remember lookarounds are zero-width — they never appear in the result
    • Use lookahead in re.sub to insert without deleting surrounding text
    • Comment complex assertions with re.VERBOSE for readability

    Common Mistakes

    Bad Expecting the lookahead text to appear in the match — it never does; it's only an assertion.
    Good Put content you want to keep outside the lookahead; use the lookahead only to test context.
    Bad Using variable-width lookbehind like (?<=\d+) in Python's re — it raises an error.
    Good Use a fixed-width lookbehind, or install the regex module which supports variable-width.

    Interview Questions

    Question Short Answer
    What is a lookahead? A zero-width assertion that checks whether a pattern is (or isn't) followed by another.
    Positive vs negative lookahead? (?=...) requires the following pattern; (?!...) forbids it.
    What does "zero-width" mean? The assertion consumes no characters and isn't included in the match.
    Limitation of Python lookbehind? It must be fixed-width in the built-in re module.
    A real use for stacked lookaheads? Password validation — checking several rules from one position.

    Quick Revision

    Assertion Syntax Meaning
    Positive lookahead (?=...) Followed by
    Negative lookahead (?!...) Not followed by
    Positive lookbehind (?<=...) Preceded by
    Negative lookbehind (?<!...) Not preceded by

    Key Takeaways

    Lookaheads and lookbehinds are zero-width assertions that match text by its context — what comes before or after — without consuming it. Use (?=)/(?!) to look forward and (?<=)/(?<!) to look back, and stack them for powerful validation like passwords.