Table of Contents

    Groups

    PYTHON & REGULAR EXPRESSIONS

    Regex Groups — The Complete Guide

    Master capturing groups, named groups, non-capturing groups, and backreferences to extract and organize matched text in Python.

    Introduction

    In regular expressions, groups are portions of a pattern wrapped in parentheses (). They let you treat multiple characters as a single unit, capture parts of a match for later use, apply quantifiers to a whole sub-pattern, and reference matched text elsewhere. Groups turn a flat match into structured, labelled data — the key to pulling out exactly the pieces you need from a string.

    In one line: Groups are parentheses in a regex that let you capture, organize, and reuse parts of a match.

    Real-World Analogy

    Labelled Storage Boxes

    Imagine unpacking a shipment: instead of one big pile, you sort items into labelled boxes — "year", "month", "day". Groups do exactly this for a match: each set of parentheses is a box that catches its part of the text, ready to be picked up by number or by name.

    Prerequisites

    Before You Start

    • Python 3.x installed
    • Basic understanding of the re module (search, match, findall)
    • Familiarity with common regex tokens (\d, \w, +, *)
    • Comfort using raw strings r"..." for patterns
    • A Python shell or editor to run the examples

    Your First Capturing Group

    Wrap part of a pattern in parentheses to capture it, then read it back with .group().

    import re
    
    date = "2026-08-16"
    m = re.search(r"(\d{4})-(\d{2})-(\d{2})", date)
    
    print(m.group(0))   # 2026-08-16  (entire match)
    print(m.group(1))   # 2026        (first group)
    print(m.group(2))   # 08          (second group)
    print(m.group(3))   # 16          (third group)
    Note Group 0 is always the whole match. Groups are numbered from 1, left to right, by their opening parenthesis.

    Types of Groups

    1

    Capturing Group (...)

    The default — captures and numbers the match.

    Everything inside is remembered and retrievable by its position number via .group(n).

    2

    Named Group (?P<name>...)

    A capturing group with a readable label.

    Retrieve it by name with .group("name") instead of a fragile position number.

    3

    Non-Capturing Group (?:...)

    Groups without capturing.

    Use it to apply quantifiers or alternation to a sub-pattern without creating a numbered capture.

    Group Syntax Reference

    Syntax Meaning
    (...) Capturing group — numbered from 1
    (?P<name>...) Named capturing group
    (?:...) Non-capturing group (groups but doesn't capture)
    (?P=name) Backreference to a named group
    \1, \2 Backreference to a numbered group
    (?=...) Positive lookahead (non-capturing assertion)
    (?!...) Negative lookahead
    (?<=...) Positive lookbehind

    Named Groups

    Named groups make patterns self-documenting and robust against reordering.

    import re
    
    pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
    m = re.search(pattern, "2026-08-16")
    
    print(m.group("year"))    # 2026
    print(m.group("month"))   # 08
    print(m.group("day"))     # 16
    
    # groupdict() returns all named groups as a dictionary
    print(m.groupdict())      # {'year': '2026', 'month': '08', 'day': '16'}

    Non-Capturing Groups

    Use (?:...) when you need grouping for logic but don't want to store the result.

    import re
    
    # Group "ab" so + repeats it, but don't capture it
    m = re.search(r"(?:ab)+(\d+)", "ababab123")
    
    print(m.group(0))   # ababab123  (whole match)
    print(m.group(1))   # 123        (only the digits are captured)
    Why it matters Non-capturing groups keep your capture numbers clean and can slightly improve performance.

    Match Object Methods for Groups

    Method Returns
    m.group(0) The entire match
    m.group(n) The nth captured group
    m.group("name") A named group's value
    m.groups() A tuple of all captured groups
    m.groupdict() A dict of all named groups
    m.start(n) / m.end(n) Start/end index of group n
    m.span(n) A (start, end) tuple for group n
    import re
    
    m = re.search(r"(\w+)@(\w+)", "user@domain")
    print(m.groups())       # ('user', 'domain')
    print(m.span(1))        # (0, 4)
    print(m.start(2))       # 5

    Backreferences

    A backreference matches the same text a previous group captured — great for finding repeats.

    import re
    
    # \1 matches whatever group 1 captured — finds doubled words
    text = "hello hello world world done"
    matches = re.findall(r"\b(\w+)\s+\1\b", text)
    print(matches)   # ['hello', 'world']

    Named backreference

    import re
    
    # (?P=word) refers back to the named group "word"
    m = re.search(r"(?P<word>\w+) (?P=word)", "bye bye")
    print(m.group("word"))   # bye

    Using Groups in Replacements

    Reference captured groups in re.sub() with \1 or \g<name> to reformat text.

    import re
    
    # Reformat 2026-08-16 -> 16/08/2026
    date = "2026-08-16"
    result = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\3/\2/\1", date)
    print(result)   # 16/08/2026
    
    # Using named groups in replacement
    result2 = re.sub(
        r"(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})",
        r"\g<d>/\g<m>/\g<y>",
        date,
    )
    print(result2)  # 16/08/2026

    Groups Change findall Behaviour

    When your pattern has groups, findall returns the groups, not the whole match.

    import re
    
    text = "2026-08 and 2027-01"
    
    # No groups -> full matches
    print(re.findall(r"\d{4}-\d{2}", text))
    # ['2026-08', '2027-01']
    
    # With groups -> tuples of groups
    print(re.findall(r"(\d{4})-(\d{2})", text))
    # [('2026', '08'), ('2027', '01')]

    Counting Groups

    Group numbers are assigned by the order of opening parentheses. For a pattern with \(k\) capturing groups, valid indices for .group() range over:

    \[ n \in \{0, 1, 2, \dots, k\} \]

    where \(0\) is the full match and \(1 \dots k\) are the captures in left-to-right order. Non-capturing groups (?:...) do not consume a number.

    Best Practices

    Do This

    • Prefer named groups (?P<name>...) for readability and stability
    • Use non-capturing groups (?:...) when you only need grouping, not capturing
    • Use groupdict() to turn a match straight into a dictionary
    • Remember groups change what findall returns
    • Reference groups in re.sub to reformat rather than rebuild strings
    • Keep group nesting shallow to keep patterns readable

    Common Mistakes

    Bad Adding a capturing group inside a pattern and being surprised that findall now returns tuples instead of full matches.
    Good Use a non-capturing group (?:...) if you don't want to change findall's output.
    Bad Referencing a group that didn't participate in the match — it returns None and can crash later code.
    Good Check for None or give optional groups a sensible default before using them.

    Interview Questions

    Question Short Answer
    What is a capturing group? Parentheses that remember the matched text for retrieval by number.
    Difference between (...) and (?:...)? The first captures and numbers; the second groups without capturing.
    How do you name a group? With (?P<name>...) and read it via group("name").
    What is a backreference? \1 or (?P=name) that matches the same text a group captured.
    What does group(0) return? The entire matched substring.

    Quick Revision

    Group Syntax Access
    Capturing (...) group(1)
    Named (?P<n>...) group("n")
    Non-capturing (?:...) Not captured
    Backreference \1 / (?P=n) Reuses match
    All groups groups() / groupdict()

    Key Takeaways

    Regex groups turn a flat match into structured data. Use capturing groups to extract, named groups for clarity, non-capturing groups for pure grouping, and backreferences to match repeats. Remember: groups change what findall returns, and group(0) is always the full match.