Python Regex
Regular expressions for finding, extracting, and replacing text with Python's built-in re module. Essential for cleaning messy data, parsing logs, and validating input.
Table of Contents
Installation / Setup
re is built into Python — no install needed.
Always write patterns as raw strings (r"...") so backslashes are literal.
Core Syntax
Main functions
| Function |
Returns |
Use |
re.search(pattern, s) |
First match or None |
Find anywhere in string |
re.match(pattern, s) |
Match at start or None |
Anchored at beginning |
re.fullmatch(pattern, s) |
Match if entire string matches |
Validation |
re.findall(pattern, s) |
List of all matches |
Extract |
re.finditer(pattern, s) |
Iterator of match objects |
Large text |
re.sub(pattern, repl, s) |
New string |
Replace |
re.split(pattern, s) |
List |
Split |
re.compile(pattern) |
Compiled pattern |
Reuse |
m = re.search(r"\d+", "order #42 total 99")
m.group() # '42'
m.start() # 7 (index of match)
Pattern elements
| Element |
Matches |
. |
Any char except newline |
\d \w \s |
Digit, word char ([A-Za-z0-9_]), whitespace |
\D \W \S |
Negations of the above |
[abc] [a-z] |
Any of / range of chars |
[^abc] |
Not any of |
^ $ |
Start / end of string |
\b |
Word boundary |
a* a+ a? |
Zero+, one+, zero-or-one |
a{2,4} |
2 to 4 repetitions |
(abc) (?P<name>abc) |
Group / named group |
a\|b |
Alternation (a or b) |
\d+? |
Lazy (minimal) match |
(?=x) (?!x) |
Lookahead: followed by / not followed by |
(?<=x) (?<!x) |
Lookbehind: preceded by / not preceded by |
\1 |
Backreference to group 1 |
re.escape(s) |
Literal-match a string |
Flags
| Flag |
Effect |
re.IGNORECASE (re.I) |
Case-insensitive |
re.MULTILINE (re.M) |
^/$ match per line |
re.DOTALL (re.S) |
. matches newlines |
re.VERBOSE (re.X) |
Whitespace/comments in pattern |
re.ASCII (re.A) |
\w/\d ASCII-only |
re.search(r"error", s, re.IGNORECASE)
re.search(r"""
(?P<ip>\d+\.\d+\.\d+\.\d+) # IP address
\s+
(?P<status>\d{3}) # HTTP status
""", line, re.VERBOSE)
Common Operations
re.findall(r"-?\d+\.?\d*", "temps: -3.5, 0, 12.25") # ['-3.5', '0', '12.25']
Find email-like patterns
re.findall(r"[\w.+-]+@[\w-]+\.[\w.]+", text)
Named groups
m = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})", "date: 2024-03")
m.group("year") # '2024'
m.groupdict() # {'year': '2024', 'month': '03'}
Replace with callbacks
re.sub(r"\d{4}", lambda m: f"[{m.group()}]", "2024") # '[2024]'
Split on multiple delimiters
re.split(r"[,;\s]+", "a, b;c d") # ['a', 'b', 'c', 'd']
Practical Examples
1. Clean a messy price column
import re
raw = ["$1,299.00", "1 299", "USD 1,299.50", "n/a"]
def clean_price(s):
digits = re.sub(r"[^\d.]", "", s) # keep digits and dots
return float(digits) if digits else None
[clean_price(x) for x in raw] # [1299.0, 1299.0, 1299.5, None]
2. Parse log lines
pattern = re.compile(
r"(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}) "
r"(?P<level>INFO|ERROR|WARN) (?P<msg>.*)"
)
for line in open("app.log", encoding="utf-8"):
m = pattern.search(line)
if m and m.group("level") == "ERROR":
print(m.group("msg"))
re.findall(r"(\w+)=(\S+)", "lr=0.01 epochs=50 batch=32") # [('lr', '0.01'), ...]
Common Patterns
# Strip HTML tags (simple cases)
re.sub(r"<[^>]+>", "", html)
# Normalize whitespace
re.sub(r"\s+", " ", text).strip()
# Slugify a title
slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
# Detect if a string is a valid simple email
bool(re.fullmatch(r"[\w.+-]+@[\w-]+\.[\w.]+", email))
# Lazy vs greedy: first vs last match
re.search(r"<.*>", "<a><b>").group() # '<a><b>' (greedy)
re.search(r"<.*?>", "<a><b>").group() # '<a>' (lazy)
Best Practices
- Always use raw strings (
r"...") for patterns.
re.compile patterns used in loops for speed and clarity.
- Use
re.VERBOSE + named groups for anything non-trivial.
- Prefer
re.fullmatch for validation, re.search for finding.
- Test patterns on edge cases: empty string, Unicode,
\n, very long input.
- Use
re.escape() when matching user input literally.
- Watch for catastrophic backtracking with nested quantifiers like
(a+)+.
Common Mistakes
| Mistake |
Consequence |
Correct approach |
"\\d+" instead of r"\d+" |
Matches d literally or \ escapes |
Use raw strings |
re.match when searching anywhere |
Match fails mid-string |
Use re.search |
| Greedy quantifiers in HTML/JSON |
Over-matches across tags |
Use lazy .*? |
Unescaped . meaning "any char" |
Matches more than intended |
Escape: \. |
| Recompiling in a loop |
Slow |
re.compile once |
Complex pattern with nested */+ |
Hangs on adversarial input |
Simplify; avoid (a+)+ |
Troubleshooting
| Symptom |
Cause |
Fix |
re.error: missing ), unterminated subpattern |
Unbalanced parentheses |
Count groups; check (?:...) non-capturing |
| Pattern matches nothing |
Wrong anchor / flag |
Test on regex101; try re.search vs match |
'NoneType' object has no attribute 'group' |
No match found |
Guard: if m: m.group() |
| Backtracking timeout / freeze |
Nested quantifiers |
Simplify pattern; use possessive/atomic alternatives |
\d matches Arabic-Indic digits |
Unicode by default |
Add re.ASCII |
Official Documentation
Last Updated
2026-09-06 — Initial version.