Regex in Python
The re module: search, match and fullmatch, findall against finditer, group access, compiled patterns, and raw strings.
Three ways to start a match
| Function | Anchored how | Typical use |
|---|---|---|
re.search | anywhere in the string | find a pattern inside a larger text |
re.match | at position 0 only | legacy; prefer fullmatch for validation |
re.fullmatch | the whole string must match | validation, the equivalent of ^...$ |
re.findall | every non-overlapping match | quick extraction of plain strings |
re.finditer | every match, lazily | you need groups or offsets |
re.sub | every match replaced | rewriting text |
import re
re.search(r'\d+', 'id 42').group() # '42'
re.match(r'\d+', 'id 42') # None: match anchors at position 0
re.fullmatch(r'\d+', '42') # matches; '42 ' would not
re.search(r'\d+', 'id 42').span() # (3, 5)
re.search(r'\d+', 'id 42').start() # 3
# an optional leading sign, then digits, anchored both ends
re.fullmatch(r'[+-]?\d+(\.\d+)?', '-3.14') is not None # Truere.matchissearchrestricted to position 0; it is not full-string matching, which is whyre.fullmatchexists.- A pattern without anchors finds a fragment. Python offers no implicit whole-string mode, so validation needs
fullmatchor explicit\A...\z. - Always write patterns as raw strings:
r'\d'. Without ther, Python turns\dinto an escape sequence before the regex engine ever sees it.
Groups, findall and groupdict
import re
m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})', 'on 2026-09-18')
m.group(0) # '2026-09-18'
m.group('year') # '2026'
m.groupdict() # {'year': '2026', 'month': '09', 'day': '18'}
m.groups() # ('2026', '09', '18')
# findall changes shape depending on the number of groups
re.findall(r'\d+', 'a1 b22') # ['1', '22']
re.findall(r'(\w)(\d+)', 'a1 b22') # [('a', '1'), ('b', '22')]
re.findall(r'(\w)(\d+)', 'a1 b22')[0][0] # 'a'findallreturns strings when there are no groups, a list of strings with exactly one group, and a list of tuples with two or more. That inconsistency is the main reason to preferfinditer.finditeryields match objects, so you keep the offsets and the named groups:[(m.group('year'), m.start()) for m in ...].- An unmatched optional group is
None. Test it, do not assume a string. - Use
re.subwith a function when the replacement depends on the match:re.sub(pattern, lambda m: m.group(1).upper(), text).
Compiling and the cache
import re
# the module caches the last few hundred patterns, so re.search is not slow
re.search(r'\d+', text)
# a compiled pattern is explicit, reusable and lets you attach flags once
WORD = re.compile(r'\b\w+\b', re.IGNORECASE)
WORD.findall('Hello there') # ['Hello', 'there']
# verbose patterns with comments are far easier to maintain
PHONE = re.compile(r'''
\A (?P<area> \d{3} )
-
(?P<num> \d{4} )
\z
''', re.VERBOSE)
PHONE.match('555-1234').group('num') # '1234'💡
Python 3.11 and later refuse to parse certain nested-set patterns that older versions accepted, and
re raises re.error at compile time rather than at match time. Compile your patterns at import so a broken pattern fails immediately instead of during a request.FAQ
Why does my raw string look different in the debugger?
A raw string keeps the backslashes the regex engine needs. Without the
r prefix, Python processes the escapes first, so '\d' reaches the engine as 'd' and matches a literal letter d.search or fullmatch for validation?
Use
fullmatch. search returns a match for any fragment, which means an email pattern without anchors accepts a string full of other garbage.Related
Regex in JavaScript Practical patterns: validation, logs and URLs
Last refreshed 2026-09-18.