55.8 The re Module: compile, match, search, findall, finditer, sub, split
The re module is Python’s primary interface for working with regular expressions, providing a suite of functions and classes to perform pattern matching and substitution. Understanding the nuances of its core functions is essential for writing efficient and robust text-processing code. A critical concept underpinning the module is the use of compiled regular expression objects. While the module offers convenience functions like re.match() and re.search() that take a pattern string as their first argument, internally, each call to these functions compiles the pattern string into a regex object. This compilation step involves parsing the pattern string, validating its syntax, and converting it into a finite state machine for efficient matching. For patterns used repeatedly—especially within loops—this repeated compilation becomes a significant performance drain.
Compiling Patterns with re.compile()
The re.compile() function is used to pre-compile a pattern string into a reusable Pattern object. This object has methods (match(), search(), findall(), etc.) that correspond to the module-level functions but do not require the pattern string to be passed again. This offers two main advantages: performance improvement through the avoidance of repeated compilation and improved readability by assigning a descriptive name to the pattern.
import re
# Inefficient: pattern compiled on every loop iteration
for line in lines:
if re.search(r'\d{3}-\d{2}-\d{4}', line): # Compiled each time
print("Found SSN")
# Efficient: pattern compiled once and reused
ssn_pattern = re.compile(r'\d{3}-\d{2}-\d{4}') # Compiled once
for line in lines:
if ssn_pattern.search(line): # Uses pre-compiled object
print("Found SSN")
Matching vs. Searching: re.match() and re.search()
A common point of confusion is the distinction between re.match() and re.search(). The match() function checks for a match only at the beginning of the string. It will not scan through the string looking for a match elsewhere. Conversely, search() scans the entire string until it finds the first location where the pattern matches, regardless of position.
text = "999 is a number, but so is 1001."
# match() only looks at the start
result_match = re.match(r'\d+', text) # Matches '999' at start
print(result_match.group() if result_match else "No match at start") # Output: 999
# search() looks everywhere
result_search = re.search(r'\d+', text) # Also finds '999' first
print(result_search.group()) # Output: 999
# A pattern that doesn't match the start
result_no_match = re.match(r'is', text) # Fails; 'is' is not at start
print(result_no_match is None) # Output: True
result_search_is = re.search(r'is', text) # Succeeds; finds 'is'
print(result_search_is.group()) # Output: is
The key takeaway is to use match() when you need to enforce that the pattern must be at the string’s beginning, and search() when the pattern can be anywhere.
Finding All Matches: re.findall() and re.finditer()
For extracting every occurrence of a pattern, the re module provides findall() and finditer(). The findall() function returns a list of all non-overlapping matches. If the pattern contains capturing groups, findall() returns a list of tuples containing the groups instead of the full match, which can be a surprising pitfall. The finditer() function returns an iterator yielding Match objects for each match, providing access to the full match object details (like .group(), .start(), .span()) for every occurrence.
text = "Prices: $10.99, $5.50, $25.00"
# findall() returns a list of strings
all_prices = re.findall(r'\$\d+\.\d{2}', text)
print(all_prices) # Output: ['$10.99', '$5.50', '$25.00']
# Pattern with capturing group - findall returns captured groups only
currency_digits = re.findall(r'\$(\d+\.\d{2})', text)
print(currency_digits) # Output: ['10.99', '5.50', '25.00'] (no '$')
# finditer() returns Match objects for more information
for match in re.finditer(r'\$(\d+)\.(\d{2})', text):
print(f"Full: {match.group(0)}, Dollars: {match.group(1)}, Cents: {match.group(2)}")
# Output:
# Full: $10.99, Dollars: 10, Cents: 99
# Full: $5.50, Dollars: 5, Cents: 50
# Full: $25.00, Dollars: 25, Cents: 00
Use findall() for simple extraction of matching substrings and finditer() when you need detailed information about each match’s location or captured groups.
Substitution and Splitting: re.sub() and re.split()
The re.sub() function is used for search-and-replace operations. It replaces all occurrences of a pattern in a string with a replacement string. The replacement string can reference captured groups using backreferences like \1, \g<1>, or \g<name>. The re.split() function splits a string by the occurrences of a pattern, offering much more powerful and flexible delimitation than the standard str.split() method.
text = "John Doe: johndoe@email.com, Jane Smith: janesmith@email.com"
# sub() with a backreference to anonymize names
anonymized = re.sub(r'(\w+) (\w+):', '*** ***:', text)
print(anonymized) # Output: *** ***: johndoe@email.com, *** ***: janesmith@email.com
# sub() with a named group backreference
better_anonymized = re.sub(r'(?P<first>\w+) (?P<last>\w+):', '\g<first>.[REDACTED]:', text)
print(better_anonymized) # Output: John.[REDACTED]: johndoe@email.com, Jane.[REDACTED]: janesmith@email.com
# split() on multiple different punctuation marks
data = "apple;banana,cherry.orange"
split_result = re.split(r'[;,.]+', data)
print(split_result) # Output: ['apple', 'banana', 'cherry', 'orange']
A critical best practice with re.sub() is to use raw strings (r'') for the replacement argument when it contains backslashes for backreferences to avoid unintended escape sequences.
Best Practices and Common Pitfalls
- Always Use Raw Strings for Patterns: Regular expressions heavily use the backslash (
\). In Python string literals, the backslash is also an escape character. Using a raw string (r'') prevents Python from interpreting backslashes, ensuring they are passed correctly to the regex engine.re.compile(r'\n')looks for a newline character;re.compile('\n')is identical and also correct, butre.compile('\\n')is an error-prone and confusing equivalent. - Pre-compile Repeated Patterns: As established, compiling once and reusing a
Patternobject is a fundamental performance optimization for patterns used more than a few times. - Be Cautious with the Greedy Quantifier: Quantifiers (
*,+,?,{m,n}) are greedy by default, meaning they match as much text as possible. This often leads to unexpectedly large matches. Use the non-greedy variants (*?,+?,??,{m,n}?) to match as little text as possible.text = "<title>My Page</title>" greedy_match = re.search(r'<.*>', text).group() # Output: '<title>My Page</title>' non_greedy_match = re.search(r'<.*?>', text).group() # Output: '<title>' - Understand re.findall() Behavior with Groups: If your pattern contains one or more capturing groups,
re.findall()returns a list of captured tuples, not a list of the full matches. If you need the full matches alongside groups, usefinditer()instead. - Anchor Patterns Appropriately: To ensure a pattern matches exactly what you intend, use anchors like
^(start of string) and$(end of string) to prevent unintended partial matches within larger text.