Python Strings Group — Regex Capturing Groups
Learn how Python regex capturing groups work: group(), groups(), named groups, non-capturing groups, and backreferences with clear examples.
When Python's re module finds a match inside a string, it gives you back a match object. The match object is more than a simple yes/no result — it remembers every sub-pattern you wrapped in parentheses, called a capturing group. The methods .group() and .groups() are how you retrieve those captured pieces.
This chapter covers everything you need to use groups confidently: numbered groups, named groups, non-capturing groups, backreferences in substitutions, and common pitfalls.
What Is a Capturing Group?
A capturing group is a part of a regex pattern wrapped in plain parentheses (...). When the pattern matches, Python saves the text matched by each pair of parentheses separately, in addition to saving the whole match.
import re
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2024-03-15')
print(m.group(0)) # 2024-03-15 — the whole match
print(m.group(1)) # 2024 — first group
print(m.group(2)) # 03 — second group
print(m.group(3)) # 15 — third groupGroups are numbered from left to right, starting at 1, in the order their opening parenthesis appears. group(0) (or just group() with no argument) always returns the entire match.
The .group() Method
.group(n) returns the text matched by the nth capturing group. You can pass multiple indices at once to get a tuple of results.
import re
m = re.search(r'(\w+)@(\w+)\.(\w+)', '[email protected]')
# Individual groups
print(m.group(1)) # alice
print(m.group(2)) # example
print(m.group(3)) # com
# Multiple at once
print(m.group(1, 3)) # ('alice', 'com')If a group exists in the pattern but did not participate in the match (for example, it was inside an optional section), group(n) returns None.
import re
# group(1) is optional — it may not match
m = re.search(r'(\d+)?-(\d+)', 'abc-456')
print(m.group(1)) # None (the optional \d+ did not match)
print(m.group(2)) # 456The .groups() Method
.groups() returns a tuple containing the text matched by every capturing group, in order. It is a convenient shortcut when you want all groups at once.
import re
m = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2024-03-15')
print(m.groups()) # ('2024', '03', '15')Providing a Default for Unmatched Groups
If a group did not participate in the match, it appears as None in the tuple. You can supply a default argument to replace those None values with something more useful.
import re
m = re.search(r'(\d+)?-(\d+)', 'abc-456')
print(m.groups()) # (None, '456')
print(m.groups(default='0')) # ('0', '456')Named Groups with (?P<name>...)
Numbered groups become hard to track in complex patterns. Named groups let you assign a meaningful label to each group using the (?P<name>...) syntax, then retrieve the value by name instead of by position.
import re
pattern = r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
m = re.search(pattern, '2024-03-15')
print(m.group('year')) # 2024
print(m.group('month')) # 03
print(m.group('day')) # 15Named groups still have a number, so you can access them either way.
The .groupdict() Method
When you have named groups, .groupdict() returns a dictionary mapping each name to its matched text. This is useful when you want to unpack a match into a structured object.
import re
m = re.search(
r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})',
'2024-03-15'
)
print(m.groupdict())
# {'year': '2024', 'month': '03', 'day': '15'}You can then use the result directly:
date_parts = m.groupdict()
print(f"{date_parts['day']}/{date_parts['month']}/{date_parts['year']}")
# 15/03/2024Non-Capturing Groups with (?:...)
Sometimes you need to group part of a pattern for the purpose of applying a quantifier or an alternation, but you do not want that group to appear in the match results. Use (?:...) — a non-capturing group.
import re
# Without non-capturing group: both parts appear in groups()
m1 = re.search(r'(\d{4})-(\d{2})-(\d{2})', '2024-03-15')
print(m1.groups()) # ('2024', '03', '15')
# Year grouped but not captured — month and day are groups 1 and 2
m2 = re.search(r'(?:\d{4})-(\d{2})-(\d{2})', '2024-03-15')
print(m2.groups()) # ('03', '15')Non-capturing groups are a best practice when you do not need a particular sub-match — they keep your group indices clean and avoid wasting memory.
Nested Groups
Groups can be nested. The numbering is always based on the position of the opening parenthesis, counting from left to right.
import re
# ((\d{4})-(\d{2}))-(\d{2})
# group 1 = the outer (year-month pair)
# group 2 = year
# group 3 = month
# group 4 = day
m = re.search(r'((\d{4})-(\d{2}))-(\d{2})', '2024-03-15')
print(m.group(1)) # 2024-03
print(m.group(2)) # 2024
print(m.group(3)) # 03
print(m.group(4)) # 15
print(m.groups()) # ('2024-03', '2024', '03', '15')Groups with re.findall()
When you use re.findall() and the pattern contains exactly one capturing group, it returns a list of strings — one per match.
When the pattern contains two or more capturing groups, it returns a list of tuples.
import re
# One group → list of strings
emails = '[email protected], [email protected]'
usernames = re.findall(r'(\w+)@\w+\.\w+', emails)
print(usernames) # ['alice', 'bob']
# Two groups → list of tuples
pairs = re.findall(r'(\w+)@(\w+)\.com', emails)
print(pairs) # [('alice', 'gmail'), ('bob', 'yahoo')]Groups with re.finditer()
re.finditer() returns an iterator of match objects, so you can access .group() and .groups() on each one individually. This is the most flexible approach when you need full match details for every occurrence.
import re
text = '[email protected], [email protected]'
for m in re.finditer(r'(?P<user>\w+)@(?P<domain>\w+)\.com', text):
print(m.group('user'), '->', m.group('domain'))
# alice -> gmail
# bob -> yahooBackreferences in re.sub()
Capturing groups also power backreferences in re.sub(). In the replacement string, \1, \2, … refer to the text captured by group 1, group 2, and so on. Named groups use \g<name>.
import re
# Swap first and last name
result = re.sub(r'(\w+)\s(\w+)', r'\2 \1', 'John Smith')
print(result) # Smith John
# Same using named groups
result2 = re.sub(
r'(?P<first>\w+)\s(?P<last>\w+)',
r'\g<last> \g<first>',
'Jane Doe'
)
print(result2) # Doe JanePosition of a Group: .start(), .end(), .span()
Match objects expose the position of every group, not just its text. Pass a group number (or name) to .start(), .end(), or .span().
import re
m = re.search(r'(\d{4})-(\d{2})', '2024-03')
print(m.span(1)) # (0, 4) — year occupies indices 0..3
print(m.start(2)) # 5 — month starts at index 5
print(m.end(2)) # 7 — month ends before index 7Quick Reference
| Method | Returns | Notes |
|---|---|---|
m.group() or m.group(0) | Whole match string | Always available |
m.group(n) | Text matched by group n | None if group did not match |
m.group(n, m, ...) | Tuple of groups | Multiple indices in one call |
m.groups() | Tuple of all groups | Optional default= for unmatched |
m.groupdict() | Dict of named groups | Only named groups appear |
m.start(n) / m.end(n) | Start / end index of group n | Pass 0 for whole match |
m.span(n) | (start, end) tuple | Shorthand for both |
Common Mistakes
Using .group() without checking for None. re.search() returns None when there is no match, so calling .group() on the result directly raises AttributeError. Always guard the call:
import re
m = re.search(r'(\d+)', 'no digits here')
if m:
print(m.group(1))
else:
print('no match')
# no digits hereConfusing .group() and .groups(). .group(1) returns a string; .groups() always returns a tuple. Mixing them up causes subtle type errors.
Forgetting that re.findall() changes shape with groups. Without groups it returns a flat list of strings; with groups it returns a list of tuples. Add or remove a capturing group and your code that processes the result may break silently.
When to Use Each Approach
- Use numbered groups for simple, short patterns with few groups.
- Use named groups when patterns are complex, or when you will use the result over several lines of code — the names act as self-documentation.
- Use non-capturing groups
(?:...)whenever you need grouping only to apply a quantifier or alternation, not to extract a value. - Use
.groups()to unpack all captures at once into a tuple. - Use
.groupdict()when all groups are named and you want a dictionary.
For a broader overview of the re module and its pattern syntax, see the Python RegEx chapter. For string operations that do not require regex, see Modify Strings and Python String Methods.