Work with Regular Expressions in Python

Learn how to use Python's re module to search, match, replace, and split text using regular expression patterns.

8 min read

⚠ 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.
Rune AI

Rune AI

Key Insights

  • Use re.search(pattern, text) to find a pattern anywhere in a string.
  • Use raw strings like r'\\d+' for all regex patterns to avoid escape character confusion.
  • re.findall() returns all non-overlapping matches; re.finditer() returns an iterator of match objects.
  • Use re.sub(pattern, replacement, text) to replace matches with new text.
  • Use capturing groups ( ) to extract specific parts of a match.
  • For simple substring checks, use Python's in operator instead of regex.
RunePowered by Rune AI

Frequently Asked Questions

Should I use re.search or re.match?

Use `re.search()` to find a pattern anywhere in the string. Use `re.match()` only when the pattern must appear at the very beginning of the string. `re.fullmatch()` requires the entire string to match the pattern. In most cases, `re.search()` is what you want.

What does the 'r' prefix mean in regex patterns?

`r'\d+'` is a raw string. Without the `r`, Python interprets backslashes like `\d` as escape sequences, so you would need to write `'\\d+'` to get the same regex. Always use raw strings for regex patterns to avoid double escaping.

Is it better to compile regex patterns with re.compile?

`re.compile()` is useful when you use the same pattern many times, as it avoids re-parsing the pattern on each call. For patterns used once or a few times, the module-level functions like `re.search()` internally cache compiled patterns, so explicit compilation is optional rather than required.

Conclusion

Regular expressions are a powerful tool for text processing, but they are also easy to misuse. Start with simple patterns and test them incrementally. Use raw strings for all regex patterns, prefer re.search() over re.match() for general searching, and remember that for simple substring checks, in is clearer and faster than regex.