|$ curl https://forge-ai.dev/api/markdown?path=docs/python/regex
$cat docs/python-—-regular-expressions.md
updated Recently·16 min read·published

Python — Regular Expressions

PythonIntermediate
Introduction

Regular expressions (regex) are a powerful tool for pattern matching and text manipulation. Python's re module provides a full regex engine based on the PCRE (Perl Compatible Regular Expressions) standard. Regex patterns let you search, validate, extract, and replace text with surgical precision.

While regex can seem cryptic at first, mastering a handful of core concepts unlocks enormous productivity gains — from parsing log files and validating user input to extracting structured data from unstructured text.

re Module Functions

The re module offers several top-level functions for working with patterns. Each serves a different use case depending on whether you need to search, match, split, or replace.

FunctionDescriptionReturns
re.search()Searches anywhere in the stringMatch object or None
re.match()Matches only at the beginningMatch object or None
re.fullmatch()Matches the entire stringMatch object or None
re.findall()Returns all non-overlapping matchesList of strings or tuples
re.finditer()Returns an iterator of match objectsIterator of Match objects
re.sub()Replaces matches with a replacementNew string
re.subn()Like sub(), but also returns count(new_string, count)
re.split()Splits string by patternList of strings
re.compile()Pre-compiles a pattern for reuseCompiled pattern object
re_search.py
Python
1import re
2
3# re.search — finds first match anywhere in string
4match = re.search(r"\d+", "There are 42 apples")
5if match:
6 print(match.group()) # "42"
7 print(match.span()) # (10, 12)
8
9# re.match — matches only at the beginning
10match = re.match(r"\d+", "42 apples")
11print(match.group()) # "42"
12
13match = re.match(r"\d+", "apples 42")
14print(match) # None — no match at start
15
16# re.fullmatch — must match the entire string
17re.fullmatch(r"\d{3}-\d{4}", "555-1234") # Match
18re.fullmatch(r"\d{3}-\d{4}", "Call 555-1234") # None
19
20# re.findall — returns all matches as a list
21emails = re.findall(r"\S+@\S+", "Contact: a@b.com or c@d.org")
22print(emails) # ['a@b.com', 'c@d.org']
23
24# re.finditer — returns iterator of match objects
25for m in re.finditer(r"\d+", "x1 y22 z333"):
26 print(f"{m.group()} at {m.span()}")
re_sub_split.py
Python
1import re
2
3# re.sub — replace matches
4text = re.sub(r"\bfoo\b", "bar", "foo foobar foo")
5print(text) # "bar foobar bar"
6
7# re.sub with a function for dynamic replacement
8def double_number(m):
9 return str(int(m.group()) * 2)
10
11result = re.sub(r"\d+", double_number, "a1 b2 c3")
12print(result) # "a2 b4 c6"
13
14# re.subn — like sub but also returns the count
15new_text, count = re.subn(r"\s+", " ", " too many spaces ")
16print(new_text) # " too many spaces "
17print(count) # 4
18
19# re.split — split by pattern
20parts = re.split(r"[,;:\s]+", "one, two; three four")
21print(parts) # ['one', 'two', 'three', 'four']
22
23# Split with capture groups — separators included
24parts = re.split(r"([,;])", "one, two; three")
25print(parts) # ['one', ',', ' two', ';', ' three']

info

Use re.search() by default — it checks the entire string. Use re.match() only when you specifically need to match at position 0. Use re.fullmatch() when validating complete strings (emails, phone numbers, etc.).
Pattern Syntax

Regex patterns combine literals, metacharacters, and quantifiers to define what to match. Here is the comprehensive reference of every pattern element.

PatternDescriptionExample
abcMatches literal charactersr"hello" matches "hello"
.Any character except newliner"a.c" matches "abc", "a1c"
\\dDigit [0-9]r"\\d+" matches "42"
\\DNon-digit [^0-9]r"\\D+" matches "abc"
\\wWord character [a-zA-Z0-9_]r"\\w+" matches "hello_42"
\\WNon-word characterr"\\W" matches " ", "@"
\\sWhitespace [ \\t\\n\\r\\f\\v]r"\\s+" matches " \\t"
\\SNon-whitespacer"\\S+" matches "word"
\\bWord boundaryr"\\bcat\\b" matches "cat" not "catch"
\\BNon-word boundaryr"\\Bcat\\B" matches in "concatenate"
\\\\Escaped special characterr"\\." matches literal "."
[abc]Character set (any one)r"[aeiou]" matches vowels
[^abc]Negated set (not a, b, or c)r"[^0-9]" matches non-digits
[a-z]Range of charactersr"[A-Za-z]" matches letters
|Alternation (OR)r"cat|dog" matches "cat" or "dog"
pattern_syntax.py
Python
1import re
2
3# Character classes in action
4text = "The price is $12.99 and the code is ABC-123"
5
6# Find all dollar amounts
7dollars = re.findall(r"\$\d+\.\d{2}", text)
8print(dollars) # ['$12.99']
9
10# Find alphanumeric codes
11codes = re.findall(r"[A-Z]+-\d+", text)
12print(codes) # ['ABC-123']
13
14# Alternation — match either pattern
15html_tags = re.findall(r"<b>|<i>|<u>", "This is <b>bold</b> and <i>italic</i>")
16print(html_tags) # ['<b>', '<i>']
Character Classes

Character classes let you match a set of characters with a single expression. You can use predefined classes or define your own with brackets.

character_classes.py
Python
1import re
2
3# Custom character classes with brackets
4hex_pattern = r"[0-9a-fA-F]+"
5print(re.findall(hex_pattern, "Color: #FF5733")) # ['FF5733']
6
7# Negated character classes
8non_alpha = r"[^a-zA-Z]+"
9print(re.findall(non_alpha, "Hello, World! 123")) # [', ', '! ']
10
11# Unicode property classes (Python 3.11+ with regex)
12# Basic approach with re module
13ascii_only = r"[\x00-\x7F]+"
14
15# Character class shortcuts recap
16print(re.findall(r"\d", "12 34")) # ['1', '2', '3', '4']
17print(re.findall(r"\D", "12 34")) # [' ']
18print(re.findall(r"\w", "hello-world")) # ['h','e','l','l','o','w','o','r','l','d']
19print(re.findall(r"\W", "hello-world")) # ['-']
20print(re.findall(r"\s", "a b\tc")) # [' ', '\t']
21print(re.findall(r"\S", "a b\tc")) # ['a', 'b', 'c']
22
23# Combining classes
24# Match any word, digit, or punctuation
25token_pattern = r"[\w\d\p{P}]+" # Not in re module directly
26# Practical alternative:
27token_pattern = r"[a-zA-Z0-9_]+"
28print(re.findall(token_pattern, "hello_world 42!")) # ['hello_world', '42']

warning

Inside character classes, most metacharacters lose their special meaning. The caret ^ is special only as the first character (negation). The dash - is special only between characters (range). The backslash \\ still works for escaping.
Quantifiers

Quantifiers specify how many times a character, group, or character class must be present. By default, quantifiers are greedy — they match as much as possible. Append ? to make them lazy.

QuantifierMeaningExample
*Zero or morer"ab*c" matches "ac", "abc", "abbc"
+One or morer"ab+c" matches "abc", "abbc"
?Zero or one (optional)r"colou?r" matches "color", "colour"
{n}Exactly n timesr"\\d4" matches "2026"
{n,}n or more timesr"\d{3,}" matches "100", "99999"
{n,m}Between n and m timesr"\d{1,3}" matches "1", "42", "999"
*?Zero or more (lazy)r"<.*?>" matches shortest tags
+?One or more (lazy)Matches minimal characters
quantifiers.py
Python
1import re
2
3# Greedy vs lazy quantifiers
4html = "<b>bold</b> and <i>italic</i>"
5
6# Greedy — matches everything between first < and last >
7greedy = re.findall(r"<.*>", html)
8print(greedy) # ['<b>bold</b> and <i>italic</i>']
9
10# Lazy — matches each tag individually
11lazy = re.findall(r"<.*?>", html)
12print(lazy) # ['<b>', '</b>', '<i>', '</i>']
13
14# Quantifier {n} — exact count
15phone = re.findall(r"\d{3}-\d{3}-\d{4}", "Call 555-123-4567 or 800-555-0199")
16print(phone) # ['555-123-4567', '800-555-0199']
17
18# Quantifier {n,m} — range
19ip_octet = r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}"
20ip = re.findall(ip_octet, "Server 192.168.1.1 and 10.0.0.255")
21print(ip) # ['192.168.1.1', '10.0.0.255']
22
23# Optional parts
24date = r"\d{4}[-/]\d{2}[-/]\d{2}"
25print(re.findall(date, "2026-01-15 and 2026/07/09")) # ['2026-01-15', '2026/07/09']
Groups

Parentheses create capture groups that extract substrings from matches. Named groups provide readable access, and non-capturing groups enable logical grouping without extra captures.

SyntaxDescriptionExample
(abc)Capture groupmatch.group(1)
(?P<name>abc)Named capture groupmatch.group("name")
(?:abc)Non-capturing groupGroups without capturing
(?P=name)Backreference to named groupMatches same text again
(?:...|...)Non-capturing alternationGroup without extra match
groups.py
Python
1import re
2
3# Named groups — the clearest approach
4date_pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
5m = re.match(date_pattern, "2026-07-09")
6if m:
7 print(m.group("year")) # "2026"
8 print(m.group("month")) # "07"
9 print(m.group("day")) # "09"
10 print(m.groupdict()) # {'year': '2026', 'month': '07', 'day': '09'}
11
12# Numeric groups
13m = re.search(r"(\d{3})-(\d{3})-(\d{4})", "555-123-4567")
14print(m.group(0)) # "555-123-4567" (full match)
15print(m.group(1)) # "555"
16print(m.group(2)) # "123"
17print(m.group(3)) # "4567"
18print(m.groups()) # ('555', '123', '4567')
19
20# Non-capturing group — groups without capturing
21pattern = r"(?:https?|ftp)://\S+"
22urls = re.findall(pattern, "Visit https://a.com or ftp://b.org")
23print(urls) # ['https://a.com', 'ftp://b.org']
24
25# Backreference — match repeated words
26pattern = r"(?P<word>\w+)\s+(?P=word)"
27doubles = re.findall(pattern, "the the quick brown fox fox")
28print(doubles) # ['the', 'fox']
Anchors

Anchors match positions, not characters. They ensure a pattern appears at the start, end, or a specific boundary of the string or line.

AnchorPositionWith re.MULTILINE
^Start of stringStart of each line
$End of stringEnd of each line
\\AStart of string (always)Start of string only
\\ZEnd of string (always)End of string only
anchors.py
Python
1import re
2
3# ^ matches start of string
4print(re.match(r"^Hello", "Hello World")) # Match
5print(re.match(r"^World", "Hello World")) # None
6
7# $ matches end of string
8print(re.search(r"World$", "Hello World")) # Match
9print(re.search(r"Hello$", "Hello World")) # None
10
11# Without MULTILINE, ^ and $ match entire string
12print(re.findall(r"^\w+", "line1\nline2\nline3")) # ['line1']
13
14# With MULTILINE, ^ and $ match each line
15print(re.findall(r"^\w+", "line1\nline2\nline3", re.MULTILINE))
16# ['line1', 'line2', 'line3']
17
18# \A and \Z are not affected by MULTILINE
19print(re.match(r"\A\w+", "line1\nline2")) # Match — 'line1'
20print(re.search(r"\w+\Z", "Hello\nWorld")) # Match — 'World'
21
22# Practical: validate a full string with anchors
23is_valid_email = bool(re.fullmatch(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}", "user@example.com"))
24print(is_valid_email) # True

best practice

Always use \\A and \\Z anchors when validating entire strings, unless you need line-level matching with re.MULTILINE. They are immune to flag changes and make intent explicit.
Flags

Flags modify regex behavior. They can be passed as the third argument to re functions or embedded in the pattern with (?flags).

FlagConstantDescription
ire.IGNORECASECase-insensitive matching
mre.MULTILINE^ and $ match start/end of each line
sre.DOTALL. matches any character including newline
xre.VERBOSEIgnore whitespace and allow comments in patterns
are.ASCIIMake \\w, \\d, \\s match only ASCII characters
ure.UNICODEMake \\w, \\d, \\s match Unicode (default in Python 3)
flags.py
Python
1import re
2
3# re.IGNORECASE — case-insensitive matching
4print(re.findall(r"python", "Python PYTHON python", re.IGNORECASE))
5# ['Python', 'PYTHON', 'python']
6
7# re.DOTALL — dot matches newline
8html = "<div>\nHello\n</div>"
9print(re.findall(r"<div>(.+)</div>", html)) # [] — dot stops at newline
10print(re.findall(r"<div>(.+)</div>", html, re.DOTALL))
11# ['\nHello\n']
12
13# re.MULTILINE — anchors match per-line
14log = "ERROR: disk full\nINFO: ok\nERROR: timeout"
15errors = re.findall(r"^ERROR.*", log, re.MULTILINE)
16print(errors) # ['ERROR: disk full', 'ERROR: timeout']
17
18# re.VERBOSE — readable patterns with comments
19email_pattern = re.compile(r"""
20 ^ # Start of string
21 [a-zA-Z0-9._%+-]+ # Username
22 @ # @ symbol
23 [a-zA-Z0-9.-]+ # Domain name
24 \. # Dot
25 [a-zA-Z]{2,} # TLD
26 $ # End of string
27""", re.VERBOSE)
28
29print(bool(email_pattern.match("user@example.com"))) # True
30
31# Combining flags
32pattern = re.compile(r"hello\s+world", re.IGNORECASE | re.DOTALL)
33print(bool(pattern.match("Hello\nWorld"))) # True
🔥

pro tip

re.VERBOSE is the secret weapon for complex patterns. Use it to break long regex into labeled segments with comments. Your future self will thank you when debugging a 50-character pattern.
Common Patterns

These battle-tested patterns handle the most common real-world text processing tasks. Each is production-ready and thoroughly commented.

common_patterns.py
Python
1import re
2
3# --- Email validation ---
4email_re = re.compile(r"[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}")
5print(bool(email_re.fullmatch("user@example.com"))) # True
6
7# --- US phone numbers (multiple formats) ---
8phone_re = re.compile(r"(?:\+?1[-.\s]?)?\(?(\d{3})\)?[-.\s]?(\d{3})[-.\s]?(\d{4})")
9phones = phone_re.findall("Call 555-123-4567, (555) 234-5678, or +1.555.345.6789")
10print(phones) # [('555','123','4567'), ('555','234','5678'), ('555','345','6789')]
11
12# --- URL extraction ---
13url_re = re.compile(r"https?://[^\s<>"']+")
14urls = url_re.findall("Visit https://example.com or http://test.org/path?q=1")
15print(urls) # ['https://example.com', 'http://test.org/path?q=1']
16
17# --- Date formats (YYYY-MM-DD, MM/DD/YYYY, DD.MM.YYYY) ---
18date_re = re.compile(r"\d{4}[-/]\d{2}[-/]\d{2}|\d{2}[/.]\d{2}[/.]\d{4}")
19dates = date_re.findall("Born 12/25/1990, started 2026-01-15, end 31.12.2026")
20print(dates) # ['12/25/1990', '2026-01-15', '31.12.2026']
21
22# --- IPv4 address ---
23ip_re = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
24ips = ip_re.findall("Servers: 192.168.1.1, 10.0.0.1, 256.1.1.1")
25print(ips) # ['192.168.1.1', '10.0.0.1', '256.1.1.1']
26
27# --- HTML tags ---
28tag_re = re.compile(r"<(/?)(\w+)([^>]*?)>")
29tags = tag_re.findall('<div class="x"><br/><img src="a.png"/></div>')
30print(tags) # [('', 'div', ' class="x"'), ('/', 'br', '/'), ...]
31
32# --- Hex color codes ---
33hex_re = re.compile(r"#[0-9a-fA-F]{6}\b|#[0-9a-fA-F]{3}\b")
34colors = hex_re.findall("Colors: #FF5733, #00f, #abc123")
35print(colors) # ['#FF5733', '#00f', '#abc123']
Use CasePatternMatch Examples
Email[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}user@domain.com
Phone (US)\\(?\\d3\\)?[-.\\s]?\\d3[-.\\s]?\\d4(555) 123-4567
URLhttps?://[^\\s]+https://example.com/path
Date (ISO)\\d4[-/]\\d2[-/]\\d22026-07-09
IPv4\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b192.168.1.1
Hex Color#[0-9a-fA-F]{3,6}\b#FF5733, #00f
HTML Tag<\/?\w+[^>]*?><div class="x">
Integer-?\\d+42, -7, 0
Float-?\d+\.\d+3.14, -0.5
Whitespace\\s+spaces, tabs, newlines
Performance

Compiled patterns avoid re-parsing the regex on every call. For any pattern used more than once, always compile it first. The performance difference is significant at scale.

performance.py
Python
1import re
2import time
3
4# BAD: recompiles the pattern on every iteration
5def find_emails_slow(texts):
6 results = []
7 for text in texts:
8 results.extend(re.findall(r"[\w.]+@[\w.]+\.\w+", text))
9 return results
10
11# GOOD: compile once, reuse everywhere
12EMAIL_RE = re.compile(r"[\w.]+@[\w.]+\.\w+")
13
14def find_emails_fast(texts):
15 results = []
16 for text in texts:
17 results.extend(EMAIL_RE.findall(text))
18 return results
19
20# Benchmark
21texts = ["Contact: user@example.com"] * 100_000
22
23start = time.perf_counter()
24find_emails_slow(texts)
25slow_time = time.perf_counter() - start
26
27start = time.perf_counter()
28find_emails_fast(texts)
29fast_time = time.perf_counter() - start
30
31print(f"Without compile: {slow_time:.3f}s")
32print(f"With compile: {fast_time:.3f}s")
33print(f"Speedup: {slow_time / fast_time:.1f}x")

warning

Beware of catastrophic backtracking. Patterns like r"(a+)+b"on a string of only "a"s cause exponential time. Avoid nested quantifiers and keep patterns specific. If a pattern takes more than a second on a short string, suspect backtracking.

Performance tips:

Compile patterns used more than once
Anchor patterns with ^ and $ to reduce search space
Use specific character classes instead of . when possible
Prefer re.search() over re.match() when you need middle-of-string matches
Use non-capturing groups (?:...) when you don't need the captured value
For simple string operations, use str methods instead of regex
Profile with timeit before optimizing regex performance
Alternatives to Regex

Regex is powerful but not always the right tool. Python built-in string methods are faster and more readable for simple operations. Know when to reach for string methods instead.

TaskRegexString Method (Better)
Check if starts withre.match(r"^prefix", s)s.startswith("prefix")
Check if ends withre.search(r"suffix$", s)s.endswith("suffix")
Find substringre.search(r"needle", s)"needle" in s
Simple replacere.sub(r"a", "b", s)s.replace("a", "b")
Split by delimiterre.split(r",", s)s.split(",")
Check if all digitsre.fullmatch(r"\\d+", s)s.isdigit()
Strip whitespacere.sub(r"^\\s+|\\s+$", "", s)s.strip()
Contains wordre.search(r"\\bword\\b", s)"word" in s.split()
alternatives.py
Python
1# String methods vs regex — when to use what
2
3text = " Hello, World! "
4
5# Stripping whitespace
6print(text.strip()) # "Hello, World!" — use this
7print(re.sub(r"^\s+|\s+$", "", text)) # "Hello, World!" — overkill
8
9# Case-insensitive check
10data = "Hello World"
11print(data.lower().startswith("hello")) # True — use this
12print(bool(re.match(r"hello", data, re.IGNORECASE))) # True — overkill
13
14# Simple split
15csv_line = "one,two,three"
16print(csv_line.split(",")) # ['one', 'two', 'three'] — use this
17print(re.split(r",", csv_line)) # Same result, slower
18
19# When regex IS necessary
20import re
21
22# Complex pattern: validate email
23email = "user@example.com"
24print(bool(re.fullmatch(r"[\w.+-]+@[\w.-]+\.[a-zA-Z]{2,}", email)))
25
26# Replace with capture groups
27full_name = "John Smith"
28formatted = re.sub(r"(\w+) (\w+)", r"\2, \1", full_name)
29print(formatted) # "Smith, John"
30
31# Extract all numbers from mixed text
32mixed = "Order #42: 3 items at $9.99 each"
33numbers = re.findall(r"\d+\.?\d*", mixed)
34print(numbers) # ['42', '3', '9.99']

best practice

Rule of thumb: if str.replace(), str.split(), str.startswith(), or in can do the job, use them. Reserve regex for patterns that involve character classes, quantifiers, groups, or multiple possible forms. Regex should feel like a scalpel, not a hammer.
$Blueprint — Engineering Documentation·Section ID: PYTHON-REGEX·Revision: 1.0