Skip to content

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.py
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.py
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.

raw_strings.py
# 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']
raw_strings.py
# 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

FunctionWhat 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.
core_functions.py
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']
core_functions.py
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 / attributeReturns
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.
match_object.py
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)
match_object.py
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.

TokenMeaning
..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

SequenceMatches
\d\dA digit (0-90-9).
\D\DA non-digit.
\w\wA word char (letters, digits, underscore).
\W\WA non-word char.
\s\sWhitespace (space, tab, newline).
\S\SNon-whitespace.
\b\bA word boundary.
\B\BA non-boundary.
\A\A / \Z\ZStart / 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).

greedy_lazy.py
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 matches
greedy_lazy.py
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 matches

Groups: capturing, named, and non-capturing

groups.py
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']
groups.py
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>:

sub_backref.py
import re
# Swap "First Last" -> "Last, First"
print(re.sub(r"(\w+)\s+(\w+)", r"\2, \1", "Ada Lovelace"))  # Lovelace, Ada
sub_backref.py
import re
# Swap "First Last" -> "Last, First"
print(re.sub(r"(\w+)\s+(\w+)", r"\2, \1", "Ada Lovelace"))  # Lovelace, Ada

Flags

Pass flags to change matching behaviour. Combine them with ||.

FlagShortEffect
re.IGNORECASEre.IGNORECASEre.Ire.ICase-insensitive matching.
re.MULTILINEre.MULTILINEre.Mre.M^^ and $$ match at each line.
re.DOTALLre.DOTALLre.Sre.S.. also matches newline.
re.VERBOSEre.VERBOSEre.Xre.XAllow whitespace and comments in the pattern.
flags.py
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')
flags.py
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.

compile.py
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']
compile.py
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

validate.py
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'
validate.py
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 write r"\d"r"\d".
  • Greedy by accident.*.* can swallow far more than intended; reach for .*?.*? or a tighter class like [^"]*[^"]*.
  • matchmatch vs searchsearchmatchmatch anchors at the start. Use searchsearch to 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

  • rere matches, 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 the MatchMatch object.
  • 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 coffee

Was this page helpful?

Let us know how we did