Skip to content

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

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.

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.

FunctionWhat 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.
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']

search, match, fullmatch, and each item from finditer return a Match object. It carries position info and captured groups.

Method / attributeReturns
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.
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)

These characters have special meaning inside a pattern.

TokenMeaning
.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.
SequenceMatches
\dA digit (0-9).
\DA non-digit.
\wA word char (letters, digits, underscore).
\WA non-word char.
\sWhitespace (space, tab, newline).
\SNon-whitespace.
\bA word boundary.
\BA non-boundary.
\A / \ZStart / end of the whole string.
sketch Catastrophic backtracking: how a regex hangs your program p5.js
The pattern is a plus inside a group that is itself repeated. On a string that ALMOST matches -- a run of a characters ending in b -- the engine must try every way of splitting that run between the inner and outer repetition before it can report failure. That is exponential: adding two characters roughly quadruples the time. Measured 8.67 ms at 18 characters and 595.77 ms at 24. The same job written without the nesting takes 5.2 microseconds.

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

Groups: capturing, named, and non-capturing

Section titled “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']

You can also reference captured groups in a replacement using \1, \2, or \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

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

FlagShortEffect
re.IGNORECASEre.ICase-insensitive matching.
re.MULTILINEre.M^ and $ match at each line.
re.DOTALLre.S. also matches newline.
re.VERBOSEre.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')

When a pattern is used repeatedly (e.g. inside a loop), pre-compile it with re.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']
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'
  • Forgetting the raw string"\d" may warn or break; always write r"\d".
  • Greedy by accident.* can swallow far more than intended; reach for .*? or a tighter class like [^"]*.
  • match vs searchmatch anchors at the start. Use search to find a pattern anywhere.
  • Catastrophic backtracking — nested quantifiers like (a+)+ on long input can hang. Keep patterns simple.

Try these in the interactive editor. Use the Hint and Show Solution buttons only after attempting them yourself.

  • re matches, searches, extracts, and replaces text with patterns.
  • Always write patterns as raw strings (r"...").
  • Learn the core functions (search, findall, finditer, sub, split) and the Match object.
  • 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.coffeeCta

pch.feedbackHeading

pch.feedbackSubheading