Concepts / Advanced Regex: Lookahead and Lookbehind Assertions

Advanced Regex: Lookahead and Lookbehind Assertions

re.findall() is a Python method that extracts all non-overlapping substrings matching a regex pattern from a string.

  • Programming

The Search Trace

A regular expression can identify several occurrences inside one string, but the result from re.findall() depends on more than whether a pattern matches. You must also understand where the next search begins and whether the pattern contains capturing groups. Those two details explain many surprising results.

What do you think happens?

Suppose a pattern finds one complete occurrence in a line. Where does re.findall() continue searching?

  • At the first character of the same match

  • At the position immediately after the completed match

  • Only at the end of the line

  • At a random later position

Reveal answer
Answer:

At the position immediately after the completed match

findall() scans from left to right, records a match, and resumes after that match ends. This produces non-overlapping results.

Start scanposition 0First matchrecordedNext searchafter match endLater matchrecorded if matchedOverlapping candidatenot returnedscan left to rightmatch endscontinue scanningoverlap skipped
Where does the next search begin after findall() records a match, and which overlapping candidates are no longer considered?

What findall() Returns

re.findall() extracts all non-overlapping substrings that match a regular expression pattern in a string. Its return structure depends entirely on the number of capturing groups in the pattern. Parentheses create capturing groups by defining a subpattern whose content is collected.

Capturing groupsReturn structureWhat each result contains
NoneList of stringsEach complete matching substring
Exactly oneList of stringsThe content of that one group, not necessarily the complete match
Two or moreList of tuplesThe captured groups in their pattern order

The number of capturing groups controls the shape and content of the findall() result.

No groupscomplete matchList of stringsfull matchesOne groupgroup contentList of stringscaptured contentTwo or more groupsgroups in orderList of tuplescaptured valuesreturnsreturnsreturns
How does the number of capturing groups change each item in the list returned by findall()?

One Line, Several Matches

Worked example

Extracting Addresses from an Email Log

An email log line contains several email addresses. Use findall() with an email-address pattern to collect every occurrence.

  1. 1
    Scan from the beginning

    The regex engine starts at position 0 and moves left to right until it finds characters that can begin an email address.

  2. 2
    Record the first match

    When the pattern matches an email address, findall() records the complete matching substring.

  3. 3
    Resume after the match

    The next search begins immediately after the first matched address ends. An overlapping candidate inside that completed match is not returned.

  4. 4
    Repeat for later occurrences

    The scan continues through the rest of the line and records each later non-overlapping email address that matches.

Result

The result is a list containing the matching email-address strings in their left-to-right order.

Email log lineone input stringLeft-to-right scanpattern searchFirst addressmatch 1Second addressmatch 2Result listall non-overlapping matchesinspectfindcontinue after matchcollect
How does findall() scan a single line and collect each matching occurrence into its result?
Key point

For a single line with multiple occurrences, read the result in two ways: first, as a sequence of matches found from left to right; second, as a structure determined by the pattern's capturing groups.

Assertions and Match Boundaries

The article title places lookahead and lookbehind assertions in the topic area, but the supplied learning material does not define their syntax or specify their matching behavior. What the material does establish is the debugging principle that the exact pattern controls what findall() matches, how many characters it includes, and what values it returns. Therefore, when an assertion is part of a pattern, inspect it as one possible cause of an unexpected match boundary, while separately counting the pattern's capturing groups.

Before contextpattern componentAssertioninspect during debuggingTarget matchcandidate resultAfter contextpattern componentCapturing groupsreturn structurepattern contextpattern conditionpattern contextcaptured values affect
Which parts of an assertion-based regex describe surrounding context, which part identifies the target, and which part determines the returned value?

Diagnosing Unexpected Lists

Before

Expected: complete matching substrings Observed: only smaller pieces of each match

The returned strings do not contain the whole expected match.

After

Check whether the pattern contains a capturing group. With one group, findall() returns the group's content rather than the complete match.

The result is interpreted according to the number of capturing groups.

Unexpected outputbeforePattern checkdoes it match the intendedtext?Group countnone, one, or multipleQuantifiersgreedy or lazyExpected outputrechecked resultisolatethen inspectthen inspectre-evaluate
Which part of the regex caused the returned values to differ from the expected output: the pattern, its capturing groups, or its quantifiers?
  • Common mistake

    Assuming findall() always returns complete matching substrings.

    What it looks like

    A pattern includes one capturing group, so the returned strings contain only that group's content.

    Why it fails

    With exactly one capturing group, findall() returns the content of that group rather than the complete match.

    Do this instead

    Count the capturing groups before interpreting the result.

  • Common mistake

    Expecting overlapping matches.

    What it looks like

    A later candidate begins inside a match that has already been recorded.

    Why it fails

    findall() resumes scanning after the completed match, so results are non-overlapping.

    Do this instead

    Trace the end of each match and mark where the next search begins.

  • Common mistake

    Ignoring quantifier behavior when match boundaries are unexpected.

    What it looks like

    A quantifier matches more or fewer characters than intended.

    Why it fails

    Quantifiers can be greedy or lazy. By default, quantifiers are greedy and match as much as possible; adding ? after a quantifier makes it lazy.

    Do this instead

    Inspect every *, +, or ? and decide whether its extent is intended.

  • Common mistake

    Debugging the returned list without testing the pattern itself.

    What it looks like

    The output differs from expectation, but the pattern does not actually match the assumed text.

    Why it fails

    The result can be correct for a pattern that was misunderstood.

    Do this instead

    Test the regex pattern in isolation first, then inspect groups and quantifiers.

A Reliable Debugging Routine

  1. 1
    Write down the exact substrings you expect the pattern to match.
  2. 2
    Test the regex pattern in isolation and check whether it matches those substrings.
  3. 3
    Trace the first match from left to right and mark where it ends.
  4. 4
    Begin the next search after that ending position; do not assume overlapping candidates will be returned.
  5. 5
    Count the capturing groups created by parentheses.
  6. 6
    Interpret a list of strings or a list of tuples according to that group count.
  7. 7
    Inspect *, +, and ? quantifiers if the match includes too many or too few characters.
  8. 8
    If an assertion is present, isolate its contribution to the pattern and verify its assertion-specific behavior using the relevant regex reference.
EASY

A findall() call returns a list of tuples, but you expected a list of complete strings. What should you inspect first, and why?

Hints
  • A list of tuples indicates that the pattern has multiple capturing groups.
  • Check the parentheses in the pattern and the order of the groups.
  • Then verify whether the complete match or only the captured groups are the values you actually need.
MEDIUM

A pattern returns fewer results than expected from one line. Trace the first match, mark its ending position, and identify any candidate that begins before that position. Explain whether that candidate overlaps an existing match.

Hints
  • findall() scans from left to right.
  • The next search begins after the current match ends.
  • An overlapping candidate will not be returned as a separate result.

Key Takeaways

  1. 1
    re.findall() returns all non-overlapping matches found by scanning a string from left to right.
  2. 2
    After recording a match, findall() resumes searching immediately after that match ends.
  3. 3
    No capturing groups or one capturing group produce a list of strings; two or more capturing groups produce a list of tuples.
  4. 4
    Unexpected output usually requires checking the pattern, the number of capturing groups, and the behavior of quantifiers.
  5. 5
    When lookahead or lookbehind assertions are present, analyze them as part of the pattern while separately checking match boundaries and return structure.

At a glance

  • findall() collects all non-overlapping matches in left-to-right order.
  • The next search begins after the previous match ends.
  • Capturing-group count determines whether the result is a list of strings or a list of tuples.
  • Debugging should proceed through pattern matching, group counting, match boundaries, and quantifier behavior.
  • The supplied material does not define the detailed syntax or consumption rules of lookahead and lookbehind assertions, so those details require a dedicated regex reference.

Related concepts

More concepts starting with A