Python re — Regular Expressions
The re module is Python’s built-in engine for regular expressions — compact patterns that describe sets of strings. Use it to search, match, extract, validate, and replace text. It ships with the standard library, so no installation is needed:
import re
text = "Contact: alice@example.com, bob@work.org"
emails = re.findall(r"[\w.+-]+@[\w-]+\.[\w.-]+", text)
print(emails)
# ['alice@example.com', 'bob@work.org']Raw strings — always use r"..."
Section titled “Raw strings — always use r"..."”Regex patterns use the backslash (\) heavily. Python also uses backslash for string escapes, so a plain string like "\b" becomes a backspace character before re ever sees it. Raw strings (prefix r) turn off Python’s escaping so the pattern reaches re intact.
# Without raw string: Python eats the backslash
print("\bword") # prints a backspace control char + 'word'
# With raw string: the regex engine receives \b (word boundary)
import re
print(re.findall(r"\bword\b", "a word here")) # ['word']Rule of thumb: write every pattern as a raw string. It costs nothing and avoids subtle bugs.
The core functions
Section titled “The core functions”| Function | What it does |
|---|---|
re.search(pattern, string) | Scan the whole string; return the first match (or None). |
re.match(pattern, string) | Match only at the start of the string. |
re.fullmatch(pattern, string) | Match only if the entire string fits the pattern. |
re.findall(pattern, string) | Return a list of all non-overlapping matches. |
re.finditer(pattern, string) | Return an iterator of Match objects (memory-friendly). |
re.sub(pattern, repl, string) | Replace all matches with repl; returns a new string. |
re.subn(pattern, repl, string) | Like sub but returns (new_string, count). |
re.split(pattern, string) | Split the string by the pattern. |
re.compile(pattern) | Pre-compile a pattern into a reusable Pattern object. |
re.escape(string) | Escape all special chars in a literal string. |
import re
s = "The year 2024 and the year 2025."
print(re.search(r"\d{4}", s).group()) # 2024 (first hit)
print(re.match(r"\d{4}", s)) # None (string starts with 'The')
print(re.findall(r"\d{4}", s)) # ['2024', '2025']
print(re.sub(r"\d{4}", "YEAR", s)) # The year YEAR and the year YEAR.
print(re.split(r",\s*", "a, b,c, d")) # ['a', 'b', 'c', 'd']The Match object
Section titled “The Match object”search, match, fullmatch, and each item from finditer return a Match object. It carries position info and captured groups.
| Method / attribute | Returns |
|---|---|
m.group(0) / m.group() | The whole match. |
m.group(n) | The text of the nth capture group. |
m.groups() | A tuple of all groups. |
m.groupdict() | A dict of all named groups. |
m.start() / m.end() | Start / end index of the match. |
m.span() | (start, end) tuple. |
import re
m = re.search(r"(\d{4})-(\d{2})-(\d{2})", "Date: 2025-01-31 ok")
print(m.group(0)) # 2025-01-31
print(m.group(1)) # 2025
print(m.groups()) # ('2025', '01', '31')
print(m.span()) # (6, 16)Metacharacters
Section titled “Metacharacters”These characters have special meaning inside a pattern.
| Token | Meaning |
|---|---|
. | Any character except newline. |
^ | Start of string (or line, with re.MULTILINE). |
$ | End of string (or line, with re.MULTILINE). |
* | 0 or more of the previous token. |
+ | 1 or more of the previous token. |
? | 0 or 1 (also makes a quantifier lazy). |
{m,n} | Between m and n repetitions. |
[...] | A character class — any one char listed. |
[^...] | Negated class — any char not listed. |
| | Alternation — “this OR that”. |
() | A capture group. |
\ | Escape a metacharacter, or start a special sequence. |
Special sequences
Section titled “Special sequences”| Sequence | Matches |
|---|---|
\d | A digit (0-9). |
\D | A non-digit. |
\w | A word char (letters, digits, underscore). |
\W | A non-word char. |
\s | Whitespace (space, tab, newline). |
\S | Non-whitespace. |
\b | A word boundary. |
\B | A non-boundary. |
\A / \Z | Start / end of the whole string. |
Quantifiers — greedy vs lazy
Section titled “Quantifiers — greedy vs lazy”By default quantifiers are greedy: they grab as much as possible. Add ? to make them lazy (as little as possible).
import re
html = "<a><b>"
print(re.findall(r"<.+>", html)) # ['<a><b>'] greedy: one big match
print(re.findall(r"<.+?>", html)) # ['<a>', '<b>'] lazy: smallest matchesGroups: capturing, named, and non-capturing
Section titled “Groups: capturing, named, and non-capturing”import re
# Named groups make code self-documenting
pattern = r"(?P<user>[\w.]+)@(?P<domain>[\w.]+)"
m = re.search(pattern, "send to jane.doe@mail.com please")
print(m.group("user")) # jane.doe
print(m.group("domain")) # mail.com
print(m.groupdict()) # {'user': 'jane.doe', 'domain': 'mail.com'}
# Non-capturing group (?:...) groups without storing a capture
print(re.findall(r"(?:ab)+", "abababxab")) # ['ababab', 'ab']You can also reference captured groups in a replacement using \1, \2, or \g<name>:
import re
# Swap "First Last" -> "Last, First"
print(re.sub(r"(\w+)\s+(\w+)", r"\2, \1", "Ada Lovelace")) # Lovelace, AdaPass flags to change matching behaviour. Combine them with |.
| Flag | Short | Effect |
|---|---|---|
re.IGNORECASE | re.I | Case-insensitive matching. |
re.MULTILINE | re.M | ^ and $ match at each line. |
re.DOTALL | re.S | . also matches newline. |
re.VERBOSE | re.X | Allow whitespace and comments in the pattern. |
import re
print(re.findall(r"cat", "Cat CAT cat", re.IGNORECASE)) # ['Cat', 'CAT', 'cat']
# VERBOSE makes complex patterns readable
phone = re.compile(r"""
(\d{3}) # area code
[-.\s]? # optional separator
(\d{3}) # prefix
[-.\s]?
(\d{4}) # line number
""", re.VERBOSE)
print(phone.search("Call 415-555-1234").groups()) # ('415', '555', '1234')Compile once, reuse many times
Section titled “Compile once, reuse many times”When a pattern is used repeatedly (e.g. inside a loop), pre-compile it with re.compile for clarity and a small speed gain.
import re
word_re = re.compile(r"\b\w+\b")
for line in ["hello world", "spam eggs"]:
print(word_re.findall(line))
# ['hello', 'world']
# ['spam', 'eggs']Practical examples
Section titled “Practical examples”import re
def is_valid_email(s):
return re.fullmatch(r"[\w.+-]+@[\w-]+\.[\w.-]+", s) is not None
print(is_valid_email("user@example.com")) # True
print(is_valid_email("not-an-email")) # False
# Extract all hashtags
print(re.findall(r"#(\w+)", "Loving #python and #regex")) # ['python', 'regex']
# Clean up extra whitespace
print(re.sub(r"\s+", " ", "too many\tspaces").strip()) # 'too many spaces'Common pitfalls
Section titled “Common pitfalls”- Forgetting the raw string —
"\d"may warn or break; always writer"\d". - Greedy by accident —
.*can swallow far more than intended; reach for.*?or a tighter class like[^"]*. matchvssearch—matchanchors at the start. Usesearchto find a pattern anywhere.- Catastrophic backtracking — nested quantifiers like
(a+)+on long input can hang. Keep patterns simple.
Practice Exercises
Section titled “Practice Exercises”Try these in the interactive editor. Use the Hint and Show Solution buttons only after attempting them yourself.
Exercise 1 – Find all numbers
Section titled “Exercise 1 – Find all numbers”Exercise 2 – Validate a phone number
Section titled “Exercise 2 – Validate a phone number”Exercise 3 – Mask sensitive words
Section titled “Exercise 3 – Mask sensitive words”Summary
Section titled “Summary”rematches, searches, extracts, and replaces text with patterns.- Always write patterns as raw strings (
r"..."). - Learn the core functions (
search,findall,finditer,sub,split) and theMatchobject. - Master metacharacters, special sequences, quantifiers (greedy vs lazy), groups, and flags.
- Pre-compile hot patterns and keep them simple to avoid backtracking blowups.
pch.coffeeTagline
pch.coffeeCtapch.feedbackHeading
pch.feedbackSubheading