Python re — Regular Expressions
The rere 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']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"..."r"..."
Regex patterns use the backslash (\\) heavily. Python also uses backslash for string escapes, so a plain string like "\b""\b" becomes a backspace character before rere ever sees it. Raw strings (prefix rr) turn off Python’s escaping so the pattern reaches rere 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']# 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
| Function | What it does |
|---|---|
re.search(pattern, string)re.search(pattern, string) | Scan the whole string; return the first match (or NoneNone). |
re.match(pattern, string)re.match(pattern, string) | Match only at the start of the string. |
re.fullmatch(pattern, string)re.fullmatch(pattern, string) | Match only if the entire string fits the pattern. |
re.findall(pattern, string)re.findall(pattern, string) | Return a list of all non-overlapping matches. |
re.finditer(pattern, string)re.finditer(pattern, string) | Return an iterator of MatchMatch objects (memory-friendly). |
re.sub(pattern, repl, string)re.sub(pattern, repl, string) | Replace all matches with replrepl; returns a new string. |
re.subn(pattern, repl, string)re.subn(pattern, repl, string) | Like subsub but returns (new_string, count)(new_string, count). |
re.split(pattern, string)re.split(pattern, string) | Split the string by the pattern. |
re.compile(pattern)re.compile(pattern) | Pre-compile a pattern into a reusable PatternPattern object. |
re.escape(string)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']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
searchsearch, matchmatch, fullmatchfullmatch, and each item from finditerfinditer return a Match object. It carries position info and captured groups.
| Method / attribute | Returns |
|---|---|
m.group(0)m.group(0) / m.group()m.group() | The whole match. |
m.group(n)m.group(n) | The text of the nth capture group. |
m.groups()m.groups() | A tuple of all groups. |
m.groupdict()m.groupdict() | A dict of all named groups. |
m.start()m.start() / m.end()m.end() | Start / end index of the match. |
m.span()m.span() | (start, end)(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)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
These characters have special meaning inside a pattern.
| Token | Meaning |
|---|---|
.. | Any character except newline. |
^^ | Start of string (or line, with re.MULTILINEre.MULTILINE). |
$$ | End of string (or line, with re.MULTILINEre.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}{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
| Sequence | Matches |
|---|---|
\d\d | A digit (0-90-9). |
\D\D | A non-digit. |
\w\w | A word char (letters, digits, underscore). |
\W\W | A non-word char. |
\s\s | Whitespace (space, tab, newline). |
\S\S | Non-whitespace. |
\b\b | A word boundary. |
\B\B | A non-boundary. |
\A\A / \Z\Z | Start / end of the whole string. |
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 matchesimport 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
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']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\1, \2\2, or \g<name>\g<name>:
import re
# Swap "First Last" -> "Last, First"
print(re.sub(r"(\w+)\s+(\w+)", r"\2, \1", "Ada Lovelace")) # Lovelace, Adaimport re
# Swap "First Last" -> "Last, First"
print(re.sub(r"(\w+)\s+(\w+)", r"\2, \1", "Ada Lovelace")) # Lovelace, AdaFlags
Pass flags to change matching behaviour. Combine them with ||.
| Flag | Short | Effect |
|---|---|---|
re.IGNORECASEre.IGNORECASE | re.Ire.I | Case-insensitive matching. |
re.MULTILINEre.MULTILINE | re.Mre.M | ^^ and $$ match at each line. |
re.DOTALLre.DOTALL | re.Sre.S | .. also matches newline. |
re.VERBOSEre.VERBOSE | re.Xre.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')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
When a pattern is used repeatedly (e.g. inside a loop), pre-compile it with re.compilere.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']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
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'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
- Forgetting the raw string —
"\d""\d"may warn or break; always writer"\d"r"\d". - Greedy by accident —
.*.*can swallow far more than intended; reach for.*?.*?or a tighter class like[^"]*[^"]*. matchmatchvssearchsearch—matchmatchanchors at the start. Usesearchsearchto find a pattern anywhere.- Catastrophic backtracking — nested quantifiers like
(a+)+(a+)+on long input can hang. Keep patterns simple.
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
Exercise 2 – Validate a phone number
Exercise 3 – Mask sensitive words
Summary
rerematches, searches, extracts, and replaces text with patterns.- Always write patterns as raw strings (
r"..."r"..."). - Learn the core functions (
searchsearch,findallfindall,finditerfinditer,subsub,splitsplit) and theMatchMatchobject. - Master metacharacters, special sequences, quantifiers (greedy vs lazy), groups, and flags.
- Pre-compile hot patterns and keep them simple to avoid backtracking blowups.
If this helped you, consider buying me a coffee ☕
Buy me a coffeeWas this page helpful?
Let us know how we did
