LESSON 169 · PRACTICAL PYTHON25 min

Python re.findall: Return Full Matches Instead of Groups

Extract complete ERR- and WARN- codes without returning only captured prefixes or partial codes.

Example check: Worked and guided code samples are checked against CPython 3.13. Practice uses Pyodide 0.29.5 (Python 3.13) in your browser. How we check lessons

Video lesson: Python re.findall: Return Full Matches Instead of Groups

The preview is stored on this site. YouTube loads only when you press Play. Watch on YouTube

Read the video transcript

Select a timestamp to watch that moment on YouTube.

  1. 00:00

    Here is a log line containing ERR-101 and WARN-204. We want a list of complete codes in the same order they appear. Python re.findall sounds like the obvious function, and it is, but the first pattern has a subtle trap. Parentheses around ERR or WARN are a capturing group. When findall sees exactly one capturing group, it returns the captured content rather than each full match. The result can look plausible because ERR and WARN are real words from the input, yet the important numeric part is missing. This lesson will reproduce that result, explain the return-shape rule, and build a pattern that recognizes only complete uppercase codes with exactly three ASCII

  2. 00:41

    digits. The broader regex guide introduces matching; this one focuses on findall's output contract. The example codes represent a simple contract, not a general log parser. We will deliberately keep that contract narrow so each match can be explained. The same method applies to other tokens once you define their exact spelling and boundaries. The broken pattern is (ERR|WARN)-[0-9]{3}. It uses alternation to accept two levels, but the parentheses capture only the level. The output is a list of ERR and WARN strings. It also accepts the first three digits of an overlong code because it has no ending

  3. 01:23

    boundary. That creates two independent defects: wrong return shape and partial-token matches. A code consumer expecting ERR-101 might make a wrong decision from an ERR-only result. Before fixing anything, we need to understand how findall decides what it returns. The rule is based on the number of capturing groups, not the number of alternatives or the number of matches in the input. Try reading the code without looking at the output: the captured alternative looks like a harmless way to group choices. The result demonstrates why a regex test should assert both the number of matches

  4. 02:03

    and the exact returned strings, not merely that the list is nonempty. The diagnostic example runs three related expressions. With a noncapturing alternative, findall returns each whole match. With one capturing group it returns the group text. With two groups it returns tuples, separating the level and number. None of those forms is universally best; choose one that matches the downstream data shape. Here we want the complete code, so the noncapturing group is appropriate. Its spelling is open parenthesis, question mark, colon. It groups ERR and WARN so the alternation stays together, but it does not add a capture to findall's result.

  5. 02:46

    We still need boundaries. Without them, XERR-101 could contain a matching substring and ERR-1012 could yield a valid-looking prefix. A two-group tuple can be useful for a structured record, such as level and numeric code stored in separate fields. We are not discarding that option. We are choosing whole strings because the caller wants to display and link the original tokens. The fixed expression is a raw Python string. It starts with a word boundary, uses a noncapturing ERR-or-WARN alternative, matches a hyphen and exactly three ASCII digits, and ends with another word boundary.

  6. 03:26

    The word boundaries prevent matching inside a larger word token. Using [0-9] states the ASCII-digit requirement explicitly; the shorthand digit class can match Unicode decimal digits that our log format does not accept. The sample now returns ERR-101 and WARN-204 as complete strings, while the overlong ERR-1012 does not appear. The function returns an empty list when there are no matches. It also preserves repeated tokens and their order because findall scans the text from left to right for non-overlapping matches. Be careful with the word boundary after the digits: it tests a transition in word-

  7. 04:07

    character status, not a full grammar for arbitrary identifiers. That is enough for the examples here, but any new punctuation or prefixes should be added to tests before changing the pattern. The final check combines several edge cases: a prefixed code, four digits, lowercase letters, non-ASCII digits, and a valid repeated code. Only tokens that satisfy the stated contract survive. Word boundaries are useful for this log format, but they have precise regex semantics: underscore counts as a word character, and punctuation differs. If your identifier rules are more complex, test surrounding characters explicitly.

  8. 04:49

    If you need the positions of these matches to highlight text, use re.finditer and inspect each Match object's start and end. If you need to change matched text, use re.sub, as the related lesson does. For collecting complete codes, findall plus noncapturing groups and tested boundaries is a concise solution. Also test empty input and input containing several adjacent valid tokens. findall returns an empty list rather than None when nothing matches. That predictable return type lets a caller iterate without special handling, while still distinguishing no matches from a parsing exception.

Understand the concept

Python re.findall returns all non-overlapping matches as a list. The result shape depends on capturing parentheses: zero groups yields whole matched strings, one group yields the captured text, and multiple groups yields tuples of captured text. A developer who writes (ERR|WARN)-[0-9]{3} often expects full codes but receives only ERR or WARN.

Use (?:ERR|WARN) to group alternatives without capturing them. The pattern r'\b(?:ERR|WARN)-[0-9]{3}\b' requires an uppercase level and exactly three ASCII digits. The boundaries reject an embedded token such as XERR-101 and a four-digit code such as ERR-1012. [0-9] is intentional here because \d also matches many Unicode decimal digits.

findall is appropriate when the matched strings are sufficient. If you need positions or surrounding context, use finditer and inspect Match objects. The related capture-groups lesson transforms tokens with re.sub; this page is about collecting complete tokens and understanding findall's return type.

The interactive task replaces a capturing pattern with a whole-match expression. It checks repeated codes, bad lengths, lowercase text, Unicode digits, and embedded fragments. Run the examples before editing so the effect of a single pair of parentheses is visible.

  • findall returns non-overlapping results
  • capturing groups change returned items
  • (?:...) groups without capturing
  • word boundaries reject partial codes

See it step by step

Read the code, predict the output, then compare it with the result.

01. See the capture-group surprise

PYTHON
import re
text = 'ERR-101 WARN-204'
print(re.findall(r'(ERR|WARN)-[0-9]{3}', text))
print(re.findall(r'(?:ERR|WARN)-[0-9]{3}', text))
EXPECTED OUTPUT
['ERR', 'WARN']
['ERR-101', 'WARN-204']

The first pattern captures levels; the second returns complete matches.

02. Compare two capture groups

PYTHON
import re
print(re.findall(r'(ERR|WARN)-([0-9]{3})', 'ERR-101 WARN-204'))
EXPECTED OUTPUT
[('ERR', '101'), ('WARN', '204')]

Two capturing groups yield tuples when separated fields are wanted.

03. Enforce complete tokens

PYTHON
import re
text = 'XERR-101 ERR-1012 WARN-204'
print(re.findall(r'\b(?:ERR|WARN)-[0-9]{3}\b', text))
EXPECTED OUTPUT
['WARN-204']

Both word boundaries protect the complete-code rule.

A closer look

Follow the reasoning, inspect each result, then try the suggested changes in the console below.

01 / 03

Understand findall's result shape

Count capturing groups before interpreting a findall result. No groups means full matches, one means one captured string per match, and two or more means tuples. Noncapturing groups do not add to this count. A noncapturing group changes only what findall returns; it does not change which alternatives the engine considers. Test result values as well as counts to catch a capture that silently alters downstream data.

The behavior is useful when intentionally extracting a field, but it is a bug when a caller needs the complete tagged code.

PYTHON
import re
text = 'ERR-101 WARN-204'
print(re.findall(r'(ERR|WARN)-[0-9]{3}', text))
print(re.findall(r'(?:ERR|WARN)-[0-9]{3}', text))
EXPECTED OUTPUT
['ERR', 'WARN']
['ERR-101', 'WARN-204']
Follow the reasoning
  1. Run the same input with capturing and noncapturing alternatives.
  2. Compare returned list elements.
02 / 03

Define complete-token boundaries

Each token must start with ERR or WARN and end after exactly three ASCII digits. Boundaries at both ends prevent an interior substring or an overlong number prefix from passing. Try adjacent punctuation and an underscore while testing boundaries. Python treats underscore as a word character, so a boundary rule should be chosen from the identifier grammar rather than copied blindly.

Word boundaries are transitions between word and nonword characters. For more complex identifiers, explicit surrounding-character checks may be needed because an underscore counts as a word character.

PYTHON
import re
print(re.findall(r'\b(?:ERR|WARN)-[0-9]{3}\b', 'XERR-101 ERR-1012 WARN-204'))
EXPECTED OUTPUT
['WARN-204']
Follow the reasoning
  1. Test an embedded prefix and an overlong suffix.
  2. Confirm neither appears.
03 / 03

Use finditer when positions matter

findall provides strings for a collection task. If a log viewer must highlight locations, finditer yields Match objects with start and end positions. Match objects also provide group values and spans, which can support highlighting or structured extraction. Use findall when that extra information would only complicate a simple list of complete strings.

A compiled pattern is useful when the same expression is applied repeatedly. Compilation does not alter the capture-group result contract.

PYTHON
import re
text = 'ERR-101, WARN-204'
print([(m.group(), m.start()) for m in re.finditer(r'\b(?:ERR|WARN)-[0-9]{3}\b', text)])
EXPECTED OUTPUT
[('ERR-101', 0), ('WARN-204', 9)]
Follow the reasoning
  1. Run finditer with the complete-token pattern.
  2. Read text and starting position from each match.

Try it in Python

Edit the example and run it. Python starts in your browser the first time you click Run.

Python console

Ready to run
Need input()? Add one value per line
Ctrl / ⌘ + Enter to run
OUTPUT
Your output appears here.
Your turnReturn complete uppercase ERR- or WARN- tokens with exactly three ASCII digits. Preserve occurrence order and repeats, and reject valid-looking prefixes inside longer tokens.
Need a hint?

Replace the capturing group with (?:ERR|WARN), then add word boundaries before the level and after the digits.

Complete the task and select Check task to verify your code.

    Quick quiz

    Three questions. You can change your answers and try again.

    01.What does findall return with exactly one capturing group?

    02.What does (?:ERR|WARN) do?

    03.Why use [0-9] for these codes?

    Typical mistakes

    Everyone meets these errors. See what causes them and how to fix them.

    Capturing the prefix accidentally

    COMMON MISTAKE
    re.findall(r'(ERR|WARN)-[0-9]{3}', text)
    FIX
    re.findall(r'(?:ERR|WARN)-[0-9]{3}', text)

    What happens: The list contains ERR and WARN rather than full codes.
    Noncapturing parentheses retain alternation without changing the return shape.

    Matching an overlong code prefix

    COMMON MISTAKE
    re.findall(r'ERR-[0-9]{3}', 'ERR-1012')
    FIX
    re.findall(r'\bERR-[0-9]{3}\b', text)

    What happens: ERR-101 appears even though the whole code has four digits.
    A boundary after the digits rejects a longer word token.

    Browse this lesson’s problem fixes

    Sources and further reading