Regular Expressions (re)
Regular expressions ("regex") describe patterns of text — validating an email address, extracting a date buried in a log line, splitting on multiple kinds of whitespace, or rewriting text that follows a shape rather than an exact string. Python exposes them through the built-in re module. Regex syntax itself is not Python-specific — the same patterns work (with minor dialect differences) in JavaScript, Java, grep, and most other languages.
Basic Pattern Syntax
A regex pattern is built from literal characters (which match themselves) and special metacharacters that match categories or repetitions of characters.
Syntax | Meaning |
|---|---|
| Any single character (except newline by default) |
| Zero or more of the preceding item |
| One or more of the preceding item |
| Zero or one of the preceding item |
| A digit ( |
| A word character (letters, digits, underscore) |
| A whitespace character (space, tab, newline) |
| Start of the string (or line, in multiline mode) |
| End of the string (or line, in multiline mode) |
| A character class, e.g. |
`match()` vs `search()` vs `findall()`
These three functions are the most common entry points into re, and mixing them up is a frequent source of bugs.
Function | Where it looks | Returns |
|---|---|---|
| Only at the very start of the string | A |
| Anywhere in the string (first match) | A |
| Anywhere in the string, all non-overlapping matches | A |
import re text = "Order #4471 shipped, invoice #4472 pending" print(re.match(r"\d+", text)) # None: string doesn't start with a digit print(re.search(r"\d+", text)) # matches the first number found anywhere print(re.findall(r"\d+", text)) # every number in the string
None <re.Match object; span=(7, 11), match='4471'> ['4471', '4472']
Groups: Capturing Parts of a Match
Parentheses () mark a group within a pattern. A successful Match object exposes each group through .group(n) — .group(0) (or just .group()) is the whole match, and .group(1), .group(2), etc. are the parenthesized sub-matches, in order.
import re
log_line = "2026-07-06 14:30:00 ERROR Connection refused"
m = re.search(r"(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) (\w+)", log_line)
if m:
print("Full match:", m.group(0))
print("Date:", m.group(1))
print("Time:", m.group(2))
print("Level:", m.group(3))Full match: 2026-07-06 14:30:00 ERROR Date: 2026-07-06 Time: 14:30:00 Level: ERROR
Numbered groups work, but they get hard to read once a pattern has more than two or three of them, and inserting a new group shifts every number after it. Named groups — (?P<name>...) — solve both problems by letting you access captures via .group("name") instead of a position.
import re
log_line = "2026-07-06 14:30:00 ERROR Connection refused"
pattern = r"(?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2}) (?P<level>\w+)"
m = re.search(pattern, log_line)
if m:
print(m.group("date"))
print(m.group("time"))
print(m.group("level"))
print(m.groupdict())2026-07-06
14:30:00
ERROR
{'date': '2026-07-06', 'time': '14:30:00', 'level': 'ERROR'}Find-and-Replace with `re.sub()`
re.sub(pattern, replacement, text) returns a new string with every match of pattern replaced by replacement. The replacement string can reference captured groups with backreferences (\\1, \\2, ...), letting you rearrange matched text rather than just deleting or replacing it outright.
import re
dates = "Meeting on 07/06/2026 and follow-up on 07/09/2026"
# Rewrite MM/DD/YYYY as YYYY-MM-DD using group backreferences
iso_dates = re.sub(r"(\d{2})/(\d{2})/(\d{4})", r"\3-\1-\2", dates)
print(iso_dates)Meeting on 2026-07-06 and follow-up on 2026-07-09
Compiling Patterns with `re.compile()`
Every top-level function like re.search(pattern, text) compiles pattern into an internal representation before matching. If you're applying the same pattern repeatedly — inside a loop over thousands of lines, for example — compiling it once with re.compile() and reusing the resulting pattern object avoids repeating that compilation work and reads more clearly at the call site.
import re
ip_pattern = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
log_lines = [
"connection from 192.168.1.10 accepted",
"connection from 10.0.0.5 rejected",
"heartbeat ok",
]
for line in log_lines:
match = ip_pattern.search(line)
if match:
print(f"Found IP {match.group()} in: {line}")Found IP 192.168.1.10 in: connection from 192.168.1.10 accepted Found IP 10.0.0.5 in: connection from 10.0.0.5 rejected
Always write regex patterns as raw strings:
r"...".match()anchors to the start of the string;search()looks anywhere;findall()returns every match as a list.Use
(...)for numbered groups and(?P<name>...)for named groups, accessed via.group(n)or.group("name").re.sub()supports backreferences (\1,\2, ...) in its replacement string to reuse captured text.Compile with
re.compile()when reusing the same pattern many times.