Named groups make complex patterns self-documenting and turn matches straight into dicts.
Numbered groups get confusing once a pattern has more than two or three. Named groups fix that:
pat = r"(?P<level>[A-Z]+) (?P<ms>\d+)ms"Access them by name with m.group("level"), m["level"], or all at once with m.groupdict(), which returns a dict ready to feed into a dataclass or a JSON record. groupdict(default="") replaces None for groups that didn't match. Named groups still have numbers too.
Inside the pattern, refer back to a named group with (?P=name); in re.sub replacements with \g<name>.
Match objects carry more than text:
m.start("ms"),m.end(),m.span("level")positions per groupm.lastgroupthe name of the last matched group, perfect for telling alternatives apart in a tokenizerm.reandm.stringthe pattern and inputm.expand(template)fills a replacement template from the match
A Match object is truthy, and since 3.11 supports m["name"] indexing syntax everywhere.
Combining finditer and groupdict is the most readable way to parse semi-structured lines such as logs into records.
Example
import re
LINE = re.compile(r"(?P<time>\d\d:\d\d) (?P<level>[A-Z]+) (?P<msg>.*)")
log = "10:01 INFO started\n10:02 ERROR disk full\n10:05 INFO done"
records = [m.groupdict() for m in LINE.finditer(log)]
print(records[1]) # {'time': '10:02', 'level': 'ERROR', 'msg': 'disk full'}
m = LINE.search(log)
print(m["level"], m.span("msg")) # INFO (11, 18)
print(m.expand(r"[\g<level>] \g<msg>")) # [INFO] started
pair = re.compile(r"(?P<q>['\"])(?P<body>.*?)(?P=q)")
print(pair.search("say 'hi' or \"yo\"")["body"]) # hiRemember
(?P<name>...)names a group; read it withm['name']orm.groupdict()(?P=name)matches the same text again inside the patternm.span(name)andm.lastgroupdescribe where and what matched