Groups
Parentheses do two jobs: group things together, and capture what they match so you can use it.
Grouping
>>> re.findall(r"ab+", "ab abb") # + applies to b only
['ab', 'abb']
>>> re.findall(r"(ab)+", "ab abab") # + applies to the GROUP
['ab', 'ab']
Hmm — that second result looks odd (['ab', 'ab'] instead of
['ab', 'abab']). That's findall's quirk with groups, explained
below. The grouping itself works: (ab)+ matches abab as one
match.
Capturing: keep what you matched
Groups numbered from 1, left to right:
>>> m = re.search(r"(\d{4})-(\d{2})-(\d{2})", "date: 2026-10-13")
>>> m.group(1), m.group(2), m.group(3)
('2026', '10', '13')
>>> m.group(0)
'2026-10-13' # group 0 is the whole match
All verified. This is extraction: pull the year, month, day out of the date separately.
The findall quirk
findall with groups returns the groups, not the whole matches:
>>> re.findall(r"(\d{4})-(\d{2})-(\d{2})", "2026-10-13 and 2025-01-01")
[('2026', '10', '13'), ('2025', '01', '01')] # tuples of groups!
Verified. If you wanted whole dates, that's the bug — and the fix
is non-capturing groups: (?:...) groups without capturing:
>>> re.findall(r"(?:\d{4})-(?:\d{2})-(?:\d{2})", "2026-10-13 and 2025-01-01")
['2026-10-13', '2025-01-01']
Verified. Rule: use (?:...) unless you actually want the
captured text. It also reads better — "I'm grouping, not keeping."
Named groups: capture with labels
>>> m = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})", "2026-10")
>>> m.group("year"), m.group("month")
('2026', '10')
Verified. (?P<name>...) — clunky syntax, wonderful readability.
In a pattern with five groups, names beat counting parentheses.
Backreferences: match the same text again
\1 means "whatever group 1 matched":
>>> re.search(r"(\w+), \1", "hello, hello").group()
'hello, hello' # \1 must equal "hello" again
>>> re.search(r"(\w+), \1", "hello, world") is None
True
Verified. Real uses: finding doubled words (\b(\w+) \1\b),
matching quote pairs ((['"])(.*?)\1 — the closing quote must match
the opening one).
sub: replace using groups
>>> re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\3/\2/\1", "2026-10-13")
'13/10/2026'
Verified. re.sub replaces matches; \1 in the replacement
inserts group 1. Date reformatting in one line — this is the
"find-and-replace with superpowers" that sells regex to skeptics.
Next: Alternation