The Python re module provides regular expression matching operations — pattern-based search, extraction, and substitution on strings — using an NFA-based regex engine supporting groups, lookaheads, lookbehinds, and Unicode.
Finding patterns in text
What you'll learn: The three most important re functions (search, match, findall) and the core pattern syntax (., *, +, ?, [], ^, $). After this tab you can write patterns for the most common text tasks: finding emails, phone numbers, URLs.
How to read this tab: Use regex101.com alongside this page — paste patterns and test strings and see matches highlighted in real time. Regex is impossible to learn by reading alone.
The re module lets you search text for patterns — not just fixed strings, but flexible descriptions like "a word starting with a capital letter" or "a phone number in any format" — using regular expressions.
Always use raw strings for patterns: r"\d+" not "\d+". Without the r prefix, you'd need to write "\\d+" because Python interprets \d as an escape sequence. Raw strings pass the backslashes straight through to the regex engine. Make it a habit: every regex pattern gets an r prefix.
Basic pattern matching
import re
text = "Priya Sharma, priya@example.com, +91-98765-43210, Mumbai 400001"
# re.search() — find first match anywhere in string
match = re.search(r"\d{6}", text) # 6-digit number
if match:
print(match.group()) # 400001
print(match.start()) # position where match starts
print(match.span()) # (start, end) tuple
# re.match() — match at the START of string only
m = re.match(r"Priya", text) # matches (starts with Priya)
m2 = re.match(r"Mumbai", text) # None (not at start)
# re.findall() — all non-overlapping matches as a list
emails = re.findall(r"[\w.]+@[\w.]+\.[a-z]{2,}", text)
print(emails) # ['priya@example.com']
# re.fullmatch() — entire string must match
valid = re.fullmatch(r"\d{6}", "400001") # match
invalid = re.fullmatch(r"\d{6}", "40000") # None — 5 digitsThe 8 pattern elements you'll use 90% of the time: . (any char), \d (digit), \w (word char), \s (whitespace), * (0+), + (1+), ? (0 or 1), {n,m} (n to m). These eight, combined with [] for character classes and () for groups, handle most real-world patterns.
Pattern syntax
| Pattern | Matches | Example |
|---|---|---|
. | Any character except newline | a.c → "abc", "axc" |
\w | Word character: [a-zA-Z0-9_] | \w+ → "hello_world" |
\d | Digit: [0-9] | \d{4} → "2026" |
\s | Whitespace: space, tab, newline | \s+ → " " |
^ | Start of string (or line) | ^Hello |
$ | End of string (or line) | world$ |
* | 0 or more of previous | ab* → "a", "ab", "abb" |
+ | 1 or more of previous | ab+ → "ab", "abb" |
? | 0 or 1 of previous | colou?r → "color", "colour" |
{n,m} | Between n and m repetitions | \d{2,4} |
[abc] | Character class | [aeiou] → any vowel |
(group) | Capture group | (\d{4})-(\d{2}) |
(?:group) | Non-capturing group | (?:http|https):// |
a|b | a or b | cat|dog |
Named groups make patterns readable. Instead of r"(\d{4})-(\d{2})-(\d{2})" with group(1), group(2), group(3) — use named groups: r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})" and access with match.group("year"). Much more readable, especially for complex patterns.
Groups and substitution
import re
# Capturing groups
date_pattern = r"(\d{4})-(\d{2})-(\d{2})"
match = re.search(date_pattern, "Today is 2026-06-01, meeting at 14:00")
if match:
year, month, day = match.groups()
print(year, month, day) # 2026 06 01
print(match.group(1)) # 2026 (group 1)
print(match.group(0)) # 2026-06-01 (entire match)
# Named groups — (?P<name>pattern)
pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
m = re.search(pattern, "2026-06-01")
print(m.group("year")) # 2026
# re.sub() — replace matches
text = "Hello World Python"
cleaned = re.sub(r"\s+", " ", text) # collapse whitespace
print(cleaned) # Hello World Python
# Use function as replacement
def upper_first(m):
return m.group().upper()
result = re.sub(r"\b[a-z]", upper_first, "hello world")
print(result) # Hello World✅ Beginner tab complete — check your understanding
- I know the difference between re.search() (anywhere in string) and re.match() (only at start)
- I can write a basic pattern using ., *, +, ?, [], ^, and $
- I can use re.findall() to get all matches as a list
- I know to use raw strings (r"pattern") to avoid double-backslash issues
Groups, substitution, and compiled patterns
What you'll learn: Capturing groups with () for extracting specific parts of a match. re.sub() for find-and-replace with patterns. Compiled patterns for performance. Flags like re.IGNORECASE and re.MULTILINE.
How to read this tab: Write a pattern that validates an Indian mobile number (10 digits starting with 6-9). Then extend it to extract the number from a longer sentence using a group.
Pre-compile patterns you use repeatedly. pattern = re.compile(r"\d+") then pattern.findall(text). The compilation step (building the NFA) happens once instead of on every call. For patterns used in a loop or called thousands of times, this is a meaningful speedup.
Compiled patterns and flags
import re
# Compile a pattern for reuse — faster in loops
email_re = re.compile(r"[\w.]+@[\w.]+\.[a-z]{2,}", re.IGNORECASE)
emails = ["priya@example.com", "RAHUL@COMPANY.COM", "not-an-email"]
for e in emails:
if email_re.fullmatch(e):
print(f"Valid: {e}")
# Common flags
re.IGNORECASE # or re.I — case-insensitive matching
re.MULTILINE # or re.M — ^ and $ match start/end of each line
re.DOTALL # or re.S — . matches newline too
re.VERBOSE # or re.X — ignore whitespace and comments in pattern
# re.VERBOSE — write readable patterns with comments
phone_re = re.compile(r"""
\+? # optional country code +
(\d{1,3}) # country code (1-3 digits)
[-\s]? # optional separator
(\d{5}) # first part of number
[-\s]? # optional separator
(\d{5}) # second part of number
""", re.VERBOSE)
# finditer — return match objects, not just strings
text = "call +91-98765-43210 or +91-87654-32109"
for match in phone_re.finditer(text):
print(match.group(), "→", match.groups())Non-greedy is essential for HTML and nested structures. .+ is greedy — it matches as much as possible. .+? is non-greedy — it matches as little as possible. Parsing HTML like <b>one</b> and <b>two</b> with <b>.+</b> gives you the whole string. With <b>.+?</b> you get each tag separately.
Lookaheads, lookbehinds, and non-greedy
import re
# Greedy vs non-greedy
text = "<b>bold</b> and <i>italic</i>"
greedy = re.findall(r"<.+>", text) # ['<b>bold</b> and <i>italic</i>']
non_greedy = re.findall(r"<.+?>", text) # ['<b>', '</b>', '<i>', '</i>']
# Lookahead (?=...) — assert what follows, without consuming
# Match "Python" only when followed by " 3"
matches = re.findall(r"Python(?= 3)", "Python 3 and Python 2")
print(matches) # ['Python'] — only the one before " 3"
# Negative lookahead (?!...)
matches = re.findall(r"Python(?! 2)", "Python 3 and Python 2")
print(matches) # ['Python'] — only the one NOT before " 2"
# Lookbehind (?<=...) — assert what precedes
prices = re.findall(r"(?<=₹)\d+", "Items: ₹500 and ₹1200")
print(prices) # ['500', '1200']r"...") and regular expressions are separate concepts. A raw string just means backslashes are not escape sequences — r"\n" is a two-character string (\ and n), not a newline. You use raw strings for regex patterns because regex also uses backslashes — \d in a raw string stays \d and is passed literally to the regex engine.re.match() only matches at the start of the string. To match anywhere, use re.search(). To require the entire string to match, use re.fullmatch(). A common mistake is using re.match() and being surprised it doesn't find a pattern in the middle of a string.r"<.+>" on "<b>text</b>" matches the entire string from first < to last >, not just <b>. Add ? to make it non-greedy: r"<.+?>".✅ Intermediate tab complete — check your understanding
- I can use () to capture groups and access them with match.group(1)
- I can use re.sub(pattern, replacement, string) for pattern-based replacement
- I know when to use re.compile() for patterns used more than once
- I know re.IGNORECASE, re.MULTILINE, and re.DOTALL flags
Lookaheads, lookbehinds, and the NFA engine
What you'll learn: Lookahead (?=) and lookbehind (?<=) assertions for context-sensitive matching. Non-greedy quantifiers. How the NFA regex engine works and why some patterns cause catastrophic backtracking.
How to read this tab: Read after you're comfortable with groups and substitution. Lookaheads especially are essential for writing patterns that match based on what comes before or after, without including that context in the match.
Catastrophic backtracking warning. Patterns like (a+)+ on a string of many "a"s can cause exponential time complexity — the regex engine keeps trying to find a match by backtracking through an exponential number of paths. Always test patterns on worst-case inputs before using in production.
The regex engine: NFA-based matching
Python's re module uses a backtracking NFA (non-deterministic finite automaton) engine. The engine tries alternatives at each branch point and backtracks when a path fails. This gives it features (backreferences, lookaheads) that DFA-based engines cannot support, but it is vulnerable to catastrophic backtracking — a regex like (a+)+ on a non-matching string of n a's takes O(2ⁿ) time. Python 3.11+ includes the re-written regex module as an alternative. The third-party regex package supports PCRE2 features like recursive patterns, possessive quantifiers (preventing backtracking: a++), and atomic groups, all of which prevent ReDoS (Regular Expression Denial of Service) vulnerabilities.
✅ Expert tab complete
- I can write a positive lookahead (?=...) to match based on what follows
- I know the difference between greedy .* and non-greedy .*?
- I can recognise a pattern that might cause catastrophic backtracking