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

FunctionAnchored howTypical use
re.searchanywhere in the stringfind a pattern inside a larger text
re.matchat position 0 onlylegacy; prefer fullmatch for validation
re.fullmatchthe whole string must matchvalidation, the equivalent of ^...$
re.findallevery non-overlapping matchquick extraction of plain strings
re.finditerevery match, lazilyyou need groups or offsets
re.subevery match replacedrewriting 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    # True
  • re.match is search restricted to position 0; it is not full-string matching, which is why re.fullmatch exists.
  • A pattern without anchors finds a fragment. Python offers no implicit whole-string mode, so validation needs fullmatch or explicit \A...\z.
  • Always write patterns as raw strings: r'\d'. Without the r, Python turns \d into 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'
  • findall returns 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 prefer finditer.
  • finditer yields 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.sub with 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.

Regex in JavaScript Practical patterns: validation, logs and URLs

Last refreshed 2026-09-18.