The re Module
Seven functions cover every regex job: search, match, fullmatch, findall, finditer, sub and split. Knowing which one to reach for is most of the skill.
- Basics
- Data Types
- Operators
- Strings
- Control Flow
- Lists
- Tuples
- Sets
- Dictionaries
- Comprehensions
- Functions
- Advanced Functions
- Recursion
- Exception Handling
- File Handling
- Modules
- Standard Library
- OOP
- Advanced OOP
- Iterators and Generators
- Decorators
- Context Managers
- Descriptors and Dataclasses
- Python Internals
- Concurrency
- Regular Expressions
- Serialization
- Command Line Python
- Testing and Debugging
- Type Hints
- Performance
- Python Security
- DSA with Python
The seven functions
| Function | Returns | Use when |
|---|---|---|
re.search | The first match, or None | Is it anywhere? |
re.match | A match at the start, or None | Does it start with this? |
re.fullmatch | A match of the whole string, or None | Validation |
re.findall | A list of strings or tuples | Every match, all at once |
re.finditer | An iterator of match objects | Every match, with positions |
re.sub | A new string | Replacing |
re.split | A list of strings | Splitting on a pattern |
search, match and fullmatch
import re
text = "order 12345 confirmed"
print(re.search(r"\d+", text)) # found at position 6
print(re.match(r"\d+", text)) # None - the string starts with 'order'
print(re.match(r"order", text)) # matches at the start
print(re.fullmatch(r"\d+", "12345")) # the entire string is digits
print(re.fullmatch(r"\d+", "123 45")) # Noneimport re
def is_valid_pin(value):
return re.fullmatch(r"\d{6}", value) is not None
for candidate in ["411001", "41100", "4110011", "41100a"]:
print(f"{candidate:<10}{is_valid_pin(candidate)}")Usefullmatchfor validation.re.match(r"\d{6}", "1234567")succeeds, becausematchonly anchors the start. That mistake accepts input it should reject.
The match object
import re
match = re.search(r"(?P<key>\w+)=(?P<value>\d+)", "config timeout=30 here")
if match:
print(match.group(0)) # timeout=30
print(match.group(1)) # timeout
print(match.group("value")) # 30
print(match.groups()) # ('timeout', '30')
print(match.groupdict()) # {'key': 'timeout', 'value': '30'}
print(match.start(), match.end(), match.span())
print(match.string[match.start():match.end()])import re
# Always test before using the result
match = re.search(r"\d+", "no digits here")
# print(match.group()) # AttributeError: 'NoneType' has no attribute 'group'
if match:
print(match.group())
else:
print("no match")
# Or with the walrus operator
if (match := re.search(r"\d+", "order 42")):
print(match.group())findall
import re
text = "a1 b22 c333"
print(re.findall(r"\d+", text)) # ['1', '22', '333'] - no groups
print(re.findall(r"([a-z])(\d+)", text)) # [('a', '1'), ('b', '22'), ('c', '333')]
print(re.findall(r"(?:[a-z])(\d+)", text)) # ['1', '22', '333'] - one group| Groups in the pattern | findall returns |
|---|---|
| None | The whole match for each hit |
| One | That group for each hit |
| Two or more | A tuple of the groups for each hit |
This changes silently when you add a group for an unrelated reason. Use (?:...) to group without capturing, or use finditer.
finditer
import re
text = "Meera scored 92, Arun scored 78, Sara scored 85"
for match in re.finditer(r"(?P<name>\w+) scored (?P<score>\d+)", text):
print(f"{match.group('name'):<8}{match.group('score'):>4}"
f" at {match.start()}")finditer gives full match objects and is lazy, so it works on very large text without building a list. Prefer it whenever you need positions, named groups, or more than a handful of matches.
sub
import re
text = "call 080-1234567 or 022-7654321"
print(re.sub(r"\d", "X", text)) # every digit
print(re.sub(r"\d{7}", "*******", text)) # the numbers only
print(re.sub(r"\s+", " ", "too many spaces"))
print(re.sub(r"\d", "X", text, count=3)) # only the first threeReferring to groups in the replacement
import re
text = "2026-08-22 and 2025-01-15"
print(re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\3/\2/\1", text))
# 22/08/2026 and 15/01/2025
print(re.sub(r"(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})",
r"\g<d>/\g<m>/\g<y>", text))A function as the replacement
import re
def double(match):
return str(int(match.group()) * 2)
print(re.sub(r"\d+", double, "a1 b22 c333")) # a2 b44 c666
def title_case(match):
return match.group().title()
print(re.sub(r"\b[a-z]+\b", title_case, "the quick brown fox"))
def redact(match):
value = match.group()
return value[:2] + "*" * (len(value) - 4) + value[-2:]
print(re.sub(r"\b\d{10,16}\b", redact, "card 1234567812345678 saved"))When the replacement depends on what was matched, pass a function. It receives the match object and returns the replacement string.
import re
text = "old value"
new_text, count = re.subn(r"old", "new", text)
print(new_text, count) # new value 1split
import re
print(re.split(r",\s*", "a, b,c, d")) # ['a', 'b', 'c', 'd']
print(re.split(r"[;,\s]+", "a,b; c d")) # several separators
print(re.split(r"(\d+)", "a1b22c")) # keeps the separators
print(re.split(r",", "a,b,c", maxsplit=1)) # ['a', 'b,c']
print("a,b,c".split(",")) # use str.split when it sufficesCompiling
import re
pattern = re.compile(r"(?P<key>\w+)\s*=\s*(?P<value>.+)")
for line in ["host = localhost", "port=8080", "not a setting"]:
match = pattern.match(line)
if match:
print(match.groupdict())
print(pattern.pattern)
print(pattern.groupindex)import re
import time
lines = ["value=%d" % i for i in range(200_000)]
start = time.perf_counter()
for line in lines:
re.search(r"\d+", line)
print(f"module level: {time.perf_counter() - start:.3f}s")
pattern = re.compile(r"\d+")
start = time.perf_counter()
for line in lines:
pattern.search(line)
print(f"compiled: {time.perf_counter() - start:.3f}s")re caches compiled patterns, so the difference is modest. Compile anyway when a pattern is used in a loop or reused across a module - it also gives the pattern a name, which documents it.
A worked example: parsing a log
import re
from collections import Counter
LOG_LINE = re.compile(r"""
^(?P<date>\d{4}-\d{2}-\d{2}) # 2026-08-22
\s
(?P<time>\d{2}:\d{2}:\d{2}) # 14:30:45
\s+
(?P<level>DEBUG|INFO|WARNING|ERROR|CRITICAL)
\s+
(?P<module>[\w.]+)
\s+
(?P<message>.+)$
""", re.VERBOSE)
log = """
2026-08-22 14:30:45 INFO app.server service started
2026-08-22 14:30:46 WARNING app.db slow query: 1.4s
2026-08-22 14:31:02 ERROR app.db connection refused
not a log line at all
2026-08-22 14:31:05 ERROR app.server request failed
"""
levels = Counter()
errors = []
skipped = 0
for line in log.strip().splitlines():
match = LOG_LINE.match(line.strip())
if not match:
skipped += 1
continue
record = match.groupdict()
levels[record["level"]] += 1
if record["level"] in {"ERROR", "CRITICAL"}:
errors.append(record)
print(dict(levels))
print(f"{skipped} unparsable lines")
for record in errors:
print(f" {record['time']} {record['module']:<12}{record['message']}")Validation patterns
import re
PATTERNS = {
"pin code": re.compile(r"\d{6}"),
"phone": re.compile(r"[6-9]\d{9}"),
"identifier": re.compile(r"[A-Za-z_]\w*"),
"hex colour": re.compile(r"#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})"),
"time 24h": re.compile(r"(?:[01]\d|2[0-3]):[0-5]\d"),
"simple email": re.compile(r"[^@\s]+@[^@\s]+\.[a-zA-Z]{2,}"),
}
def validate(kind, value):
return PATTERNS[kind].fullmatch(value) is not None
tests = [
("pin code", "411001"), ("pin code", "41100"),
("phone", "9876543210"), ("phone", "1234567890"),
("hex colour", "#a1b2c3"), ("hex colour", "#xyz"),
("time 24h", "23:59"), ("time 24h", "24:00"),
]
for kind, value in tests:
print(f"{kind:<14}{value:<14}{validate(kind, value)}")The email pattern above is deliberately simple. A fully correct email regex is enormous and still wrong at the edges. For real validation, check that there is exactly one @ with text on both sides, then send a confirmation message - that is the only test that actually matters.Common mistakes
- Using
matchwherefullmatchwas needed, accepting trailing junk. - Calling
.group()without checking forNone. - Being surprised when
findallreturns tuples after a group is added. - Forgetting that
subreturns a new string and does not modify in place. - Using
re.splitwherestr.splitwould do. - Writing one enormous pattern instead of two clear ones.
Best practices
- Use
fullmatchfor validation,searchfor finding,finditerfor many matches. - Compile patterns used more than once and name them in capitals.
- Use
re.VERBOSEwith comments for anything non-trivial. - Always check a match object before using it.
- Pass a function to
subwhen the replacement depends on the match. - Use named groups and
groupdict()to turn matches into records.
Practice
- Validate six field types with
fullmatchand report which inputs fail. - Parse a log format of your own design into dictionaries with named groups.
- Use
subwith a function to mask all but the last four digits of any long number. - Explain why adding a group changes what
findallreturns, and show it. - Rewrite a long inline pattern using
re.VERBOSEwith comments.
Conclusion
Reach for fullmatch to validate, search to find, finditer to walk every match with its position, and sub with a function to transform. Compile named patterns, use named groups, and always check the match before using it.