⚠ This article has a formatting issue and may not display correctly.
Our team has been notified. The content is shown as plain text below.
The re module in the [Python standard library](/python/what-is-the-python-standard-library) brings Python regular expressions to searching, matching, and manipulating text. Use regex when you need to find patterns like phone numbers, email addresses, or specific formats inside strings, rather than searching for exact literal substrings.
text = "Contact us at support@example.com or sales@site.org"
pattern = r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}"
matches = re.findall(pattern, text)
print(matches) # ['support@example.com', 'sales@site.org']
The pattern describes the structure of an email address: a sequence of allowed characters, an @ symbol, a domain name, a dot, and a top-level domain. re.findall() returns every non-overlapping match as a list of strings.
## Searching for patterns
Most regex work starts with locating a match before you decide what to do with it. re.search() finds the first occurrence of a pattern anywhere in a string and returns a match object, or None if nothing matches.
text = "Order #48291 shipped on 2026-07-12"
match = re.search(r"\d{4}-\d{2}-\d{2}", text)
if match:
print(match.group()) # 2026-07-12
print(match.start(), match.end()) # 24 34
r"\d{4}-\d{2}-\d{2}" matches four digits, a dash, two digits, a dash, and two digits. match.group() returns the matched text. match.start() and match.end() give the character positions where the match begins and ends.
re.match() only checks the beginning of the string:
print(re.match(r"\d+", "abc123")) # None
print(re.match(r"\d+", "123abc")) #
re.match(r"\d+", "abc123") returns None because the string does not start with digits. Use re.search() unless you specifically need to anchor the match at position 0.
## Finding all matches
re.findall() returns all non-overlapping matches as a list of strings. re.finditer() returns an iterator of match objects, which is more memory-efficient for large texts and gives access to match positions.
text = "Prices: $12.99, $5.50, $100.00"
prices = re.findall(r"\$\d+\.\d{2}", text)
print(prices)
for match in re.finditer(r"\$\d+\.\d{2}", text):
print(f"{match.group()} at position {match.start()}")
findall prints the plain list first, then the loop walks the same matches again as match objects, this time printing the character position where each one starts:
['$12.99', '$5.50', '$100.00']
$12.99 at position 8
$5.50 at position 16
$100.00 at position 23
The \$ escapes the dollar sign, which otherwise has special meaning in regex. \d+\.\d{2} matches one or more digits, a literal dot, and exactly two digits.
## Replacing text with sub
re.sub() replaces matches with new text. You can use captured groups in the replacement.
text = "Call 555-1234 or 555-5678"
masked = re.sub(r"\d{3}-\d{4}", "XXX-XXXX", text)
print(masked) # Call XXX-XXXX or XXX-XXXX
Simple replacement swaps out the whole match, but sometimes you want to keep parts of it and rearrange them. To rearrange captured groups in the replacement, use \1, \2, and so on:
text = "Date: 2026-07-12"
reformatted = re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\2/\3/\1", text)
print(reformatted) # Date: 07/12/2026
The parentheses ( ) capture groups. \1 refers to the year, \2 to the month, \3 to the day. The replacement r"\2/\3/\1" rearranges them to month/day/year format.
## Splitting with patterns
re.split() splits a string wherever a pattern matches, like str.split() but with regex patterns.
text = "apple, banana; cherry | date"
parts = re.split(r"[,;|]\s*", text)
print(parts) # ['apple', 'banana', 'cherry ', 'date']
r"[,;|]\s*" matches a comma, semicolon, or pipe followed by optional whitespace. This splits on multiple delimiters in one call, which str.split() cannot do. Notice that 'cherry ' keeps its trailing space, because the pattern only consumes whitespace after the delimiter, not before it; add \s* on both sides of the character class if you need to trim that too.
## Common regex patterns
Here are patterns you will use frequently when working with text.
| Pattern | Meaning | Example match |
|---|---|---|
| \d | Any digit (0-9) | "42" |
| \D | Any non-digit | "a" |
| \w | Word character (letter, digit, underscore) | "a", "3", "_" |
| \W | Non-word character | "!", " " |
| \s | Whitespace (space, tab, newline) | " " |
| \S | Non-whitespace | "x" |
| . | Any character except newline | "a", "9", "!" |
| + | One or more of the preceding | \d+ matches "48291" |
| * | Zero or more of the preceding | \d* matches "" or "42" |
| ? | Zero or one of the preceding | colou?r matches "color", "colour" |
| `{n}` | Exactly n of the preceding | \d{3} matches "482" |
| `{n,m}` | Between n and m of the preceding | \d{2,4} matches "48", "4829" |
| ^ | Start of string | ^Hello |
| $ | End of string | world$ |
| [abc] | One character from the set | [aeiou] |
| [^abc] | One character not in the set | [^0-9] |
## Capturing groups
Parentheses ( ) create capturing groups that let you extract specific parts of a match.
log = "ERROR 2026-07-12 14:30:05 Connection timeout"
pattern = r"(ERROR|WARN|INFO) (\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) (.+)"
match = re.search(pattern, log)
if match:
level, date, time, message = match.groups()
print(f"Level: {level}")
print(f"Date: {date}")
print(f"Time: {time}")
print(f"Message: {message}")
Unpacking match.groups() into four named variables makes the rest of the code read like plain data access instead of index lookups:
Level: ERROR
Date: 2026-07-12
Time: 14:30:05
Message: Connection timeout
match.groups() returns a tuple of all captured groups. You can also access them by index: match.group(1) is the log level, match.group(2) is the date.
Use (?: ... ) for non-capturing groups when you need grouping for alternation but do not want to capture:
re.search(r"(?:http|https)://", url)
## Compiling patterns for reuse
re.compile() turns a pattern string into a compiled regex object. Use it when the same pattern is used many times.
email_pattern = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
emails = email_pattern.findall("Contact a@b.com or c@d.org")
print(emails) # ['a@b.com', 'c@d.org']
The compiled object has the same methods as the module-level functions. Pre-compiling saves the cost of parsing the pattern on each call.
## When not to use regex
Regex is powerful but overkill for simple tasks. Use [Python's string methods](/python/common-string-methods-in-python) when they cover your need.
text = "hello world"
print("world" in text) # True
print(text.startswith("hello")) # True
print(text.endswith("world")) # True
print(text.replace("world", "Python")) # hello Python
String methods are faster, more readable, and less error-prone than regex for exact substring operations. Reach for regex when you need pattern matching, not literal search.
## Practical example: extracting data from a log file
Combine several regex techniques to parse structured log entries.
log_lines = [
"INFO 2026-07-12 10:00:01 User maya logged in from 192.168.1.42",
"ERROR 2026-07-12 10:01:15 Database connection failed after 3 retries",
"INFO 2026-07-12 10:02:00 User devin logged out",
]
pattern = re.compile(
r"(?PERROR|INFO) "
r"(?P\d{4}-\d{2}-\d{2}) "
r"(?P\d{2}:\d{2}:\d{2}) "
r"(?P.+)"
)
With the pattern compiled once outside the loop, scan each log line and print only the ones at the ERROR level, using the named groups to format a short summary:
for line in log_lines:
match = pattern.search(line)
if match:
data = match.groupdict()
if data["level"] == "ERROR":
print(f"[!] {data['date']} {data['time']}: {data['message']}")
Only the ERROR line satisfies the condition inside the loop, so it is the only one that gets printed even though all three lines match the overall pattern:
[!] 2026-07-12 10:01:15: Database connection failed after 3 retries
Named groups using (?P...) let you access matches by name with match.groupdict(). This is more readable than positional groups when the pattern has many parts.
## Common mistakes
**Using regex for literal searches.** "needle" in haystack is clearer and faster than re.search(r"needle", haystack). Reserve regex for patterns, not exact strings.
**Forgetting to escape special characters.** ., *, +, ?, [, ], (, ), {, }, ^, $, |, and \ have special meanings in regex. Escape them with \ when you want the literal character: \. matches a dot, \$ matches a dollar sign.
**Writing overly complex patterns.** A regex longer than a line or two is hard to read and debug. Break complex validation into multiple simpler checks: use Python logic to split the problem into steps and apply smaller, focused patterns at each step.
**Not using raw strings.** Without r"", backslashes in your pattern are processed as Python string escapes first. "\\d" and r"\d" are equivalent regexes, but "\d" is just the character d. Always use raw strings for regex patterns.