Regular Expressions in Python (re Module Guide)

Regular expressions in Python are patterns for finding, checking, extracting and replacing text, and they are provided by the built-in re module. re.search(r"\d+", text) finds the first number in a string, re.findall() returns every match, and re.sub() replaces them. This guide covers every re function, the pattern syntax, groups, greedy and lazy matching, flags, lookarounds, practical patterns (emails, phone numbers, dates) and the mistakes that trip up most beginners, with every example run and its real output shown.

All examples were run with Python 3.12.5 in the Windows Command Prompt. Reference: re — Regular expression operations and the Regular Expression HOWTO in the Python docs.

Your first regular expression in Python

Import re, write the pattern as a raw string (r"...") and call a function. \d means “a digit” and {4} means “exactly four times”:

import re

text = "Order #4521 shipped on 2025-03-14 to Austin, TX 78701."

match = re.search(r"\d{4}-\d{2}-\d{2}", text)     # first date-like substring
print(match.group())                              # the matched text
print(match.span())                               # where it was found

print(re.findall(r"\d+", text))                   # every run of digits

Output:

2025-03-14
(23, 33)
['4521', '2025', '03', '14', '78701']
Command Prompt output of a Python script using re.search to find a date and re.findall to list all numbers in a string
re.search() returns a match object; re.findall() returns a list of strings.

The re module functions

import re

s = "cat bat rat"

print("search   :", re.search(r"[br]at", s))       # first match anywhere
print("match    :", re.match(r"[br]at", s))        # only at the start -> None
print("fullmatch:", re.fullmatch(r"[a-z ]+", s))   # the whole string must match
print("findall  :", re.findall(r"[cbr]at", s))     # list of all matches
print("finditer :", [(m.group(), m.start()) for m in re.finditer(r"[cbr]at", s)])
print("sub      :", re.sub(r"[br]at", "dog", s))   # replace
print("subn     :", re.subn(r"[br]at", "dog", s))  # replace + count
print("split    :", re.split(r"\s+", "a  b\tc\nd"))  # split on any whitespace

Output:

search   : <re.Match object; span=(4, 7), match='bat'>
match    : None
fullmatch: <re.Match object; span=(0, 11), match='cat bat rat'>
findall  : ['cat', 'bat', 'rat']
finditer : [('cat', 0), ('bat', 4), ('rat', 8)]
sub      : cat dog dog
subn     : ('cat dog dog', 2)
split    : ['a', 'b', 'c', 'd']
Command Prompt output comparing Python re.search, re.match, re.fullmatch, re.findall, re.finditer, re.sub, re.subn and re.split on the same string
Each re function on the same text.
FunctionReturnsUse it to
re.search(p, s)Match or NoneFind the first match anywhere
re.match(p, s)Match or NoneMatch at the start of the string only
re.fullmatch(p, s)Match or NoneValidate: the whole string must match
re.findall(p, s)List of strings (or tuples with groups)Extract every match
re.finditer(p, s)Iterator of match objectsEvery match with positions, memory friendly
re.sub(p, repl, s)New stringReplace matches
re.subn(p, repl, s)(new string, count)Replace and count
re.split(p, s)List of stringsSplit on a pattern
re.compile(p)Pattern objectReuse a pattern

Splitting deserves its own guide: see how to split a string with regex in Python.

Regex syntax cheat sheet

PatternMatches
.Any character except a newline
\d / \DA digit / a non-digit
\w / \WA word character (letter, digit, _) / anything else
\s / \SWhitespace (space, tab, newline) / non-whitespace
\bA word boundary
^ / $Start / end of the string (of each line with MULTILINE)
[abc] / [^abc]One of a, b, c / any character except them
[a-z0-9]A range of characters
* + ?0 or more, 1 or more, 0 or 1
{3} {2,5} {2,}Exactly 3, 2 to 5, 2 or more
a|ba or b
( ) / (?: ) / (?P<name> )Capturing / non-capturing / named group
(?= ) (?! ) (?<= ) (?<! )Lookahead, negative lookahead, lookbehind, negative lookbehind

Each pattern above, tested with findall():

import re

samples = {
    r"\d+":        "Room 101, floor 3",
    r"\w+":        "hello_world 42!",
    r"\s":         "a b\tc",
    r"^The":       "The end. The start.",
    r"end\.$":     "The end.",
    r"colou?r":    "color colour colr",
    r"[A-Z][a-z]+": "Alice met Bob in Paris",
    r"\bcat\b":    "cat catalog bobcat cat.",
    r"a{2,3}":     "a aa aaa aaaa",
}
for pattern, text in samples.items():
    print(f"{pattern:<14} {text!r:<26} -> {re.findall(pattern, text)}")

Output:

\d+            'Room 101, floor 3'        -> ['101', '3']
\w+            'hello_world 42!'          -> ['hello_world', '42']
\s             'a b\tc'                   -> [' ', '\t']
^The           'The end. The start.'      -> ['The']
end\.$         'The end.'                 -> ['end.']
colou?r        'color colour colr'        -> ['color', 'colour']
[A-Z][a-z]+    'Alice met Bob in Paris'   -> ['Alice', 'Bob', 'Paris']
\bcat\b        'cat catalog bobcat cat.'  -> ['cat', 'cat']
a{2,3}         'a aa aaa aaaa'            -> ['aa', 'aaa', 'aaa']

Groups: extract parts of a match

Parentheses capture part of the match. Use group(n) or groups() to read them, and name them with (?P<name>...) to make the code readable:

import re

log = "2025-03-14 09:42:07 ERROR [payment] Card declined for user 1874"

pattern = r"(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) (\w+) \[(\w+)\] (.*)"
m = re.match(pattern, log)
print(m.groups())                 # all groups as a tuple
print(m.group(3), "|", m.group(5))

named = re.match(r"(?P<date>\S+) (?P<time>\S+) (?P<level>\w+)", log)
print(named.group("level"))
print(named.groupdict())

# findall returns tuples when the pattern has groups
print(re.findall(r"(\w+)=(\d+)", "width=800 height=600 depth=12"))

# non-capturing group (?:...) groups without capturing
print(re.findall(r"(?:Mr|Ms|Dr)\. (\w+)", "Dr. Patel met Ms. Garcia and Mr. Lee"))

Output:

('2025-03-14', '09:42:07', 'ERROR', 'payment', 'Card declined for user 1874')
ERROR | Card declined for user 1874
ERROR
{'date': '2025-03-14', 'time': '09:42:07', 'level': 'ERROR'}
[('width', '800'), ('height', '600'), ('depth', '12')]
['Patel', 'Garcia', 'Lee']
Command Prompt output of Python regex capturing groups, named groups with groupdict, findall returning tuples and a non-capturing group
Groups parse a log line; findall() returns tuples when the pattern has groups.

Search and replace with re.sub()

In the replacement, \1 or \g<name> inserts a group. Pass a function instead of a string to compute each replacement:

import re

# backreferences in the replacement: swap "Last, First" -> "First Last"
print(re.sub(r"(\w+), (\w+)", r"\2 \1", "Doe, Jane; Smith, John"))

# named group in the replacement
print(re.sub(r"(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})", r"\g<m>/\g<d>/\g<y>", "Due 2025-03-14"))

# a function as the replacement: add 10% to every price
print(re.sub(r"\$(\d+(?:\.\d+)?)", lambda m: f"${float(m.group(1)) * 1.1:.2f}", "Tea $4, cake $6.50"))

# collapse repeated whitespace
print(re.sub(r"\s+", " ", "too    many \t spaces\n here").strip())

Output:

Jane Doe; John Smith
Due 03/14/2025
Tea $4.40, cake $7.15
too many spaces here

Related: replace multiple spaces with a single space, remove punctuation from a string and remove special characters.

Greedy vs lazy matching

Quantifiers like + and * are greedy: they take as much text as they can. Add ? to make them lazy:

import re

html = "<b>bold</b> and <i>italic</i>"

print(re.findall(r"<.+>", html))     # greedy: as much as possible
print(re.findall(r"<.+?>", html))    # lazy: as little as possible
print(re.findall(r"<[^>]+>", html))  # a negated class is often clearer

Output:

['<b>bold</b> and <i>italic</i>']
['<b>', '</b>', '<i>', '</i>']
['<b>', '</b>', '<i>', '</i>']

Regex flags: IGNORECASE, MULTILINE, DOTALL, VERBOSE

import re

text = """Name: Alice
name: bob
NAME: Carol"""

print(re.findall(r"^name: (\w+)", text))                            # case-sensitive: "Name" != "name"
print(re.findall(r"^name: (\w+)", text, re.IGNORECASE))             # ^ = start of the whole string only
print(re.findall(r"^name: (\w+)", text, re.IGNORECASE | re.MULTILINE))  # ^ at every line

print(re.findall(r"A.+C", "A\nB\nC"))              # . does not match newline
print(re.findall(r"A.+C", "A\nB\nC", re.DOTALL))   # now it does

phone = re.compile(r"""
    \(?(\d{3})\)?    # area code, optional parentheses
    [\s.-]?          # separator
    (\d{3})          # exchange
    [\s.-]?
    (\d{4})          # line number
""", re.VERBOSE)
print(phone.findall("Call (512) 555-0147 or 512.555.0199"))

Output:

[]
['Alice']
['Alice', 'bob', 'Carol']
[]
['A\nB\nC']
[('512', '555', '0147'), ('512', '555', '0199')]
Command Prompt output of Python regex flags re.IGNORECASE, re.MULTILINE, re.DOTALL and a re.VERBOSE phone number pattern with comments
Flags change how ^, . and letter case behave; VERBOSE allows comments.

Combine flags with |, or put them inline at the start of the pattern: (?im)^name.

Lookahead and lookbehind

Lookarounds check what comes before or after a position without including it in the match. Several lookaheads in a row are a common way to test password rules:

import re

prices = "USD 120, EUR 95, USD 42.50, GBP 80"
print(re.findall(r"(?<=USD )\d+(?:\.\d+)?", prices))   # lookbehind: numbers after "USD "
print(re.findall(r"\d+(?= apples)", "3 apples, 5 pears, 12 apples"))   # lookahead

strong = re.compile(r"(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}")
for pwd in ["password", "Passw0rd", "Passw0rd!", "P0!a"]:
    print(f"{pwd:<10} strong: {bool(strong.fullmatch(pwd))}")

Output:

['120', '42.50']
['3', '12']
password   strong: False
Passw0rd   strong: False
Passw0rd!  strong: True
P0!a       strong: False

Practical patterns: emails, phone numbers, dates, URLs

import re

text = """Contact ana.lopez@example.com or support@shop.co.uk.
Call 512-555-0147. Invoice INV-2025-0042 dated 14/03/2025.
Visit https://pythonguides.com/python-tutorials/ today."""

print("emails :", re.findall(r"[\w.+-]+@[\w-]+(?:\.[\w-]+)+", text))
print("phones :", re.findall(r"\b\d{3}-\d{3}-\d{4}\b", text))
print("dates  :", re.findall(r"\b\d{2}/\d{2}/\d{4}\b", text))
print("invoice:", re.search(r"INV-(\d{4})-(\d{4})", text).groups())
print("urls   :", re.findall(r"https?://\S+?(?=[\s.,]*(?:\s|$))", text))

zip_code = re.compile(r"\d{5}(?:-\d{4})?")
for z in ["78701", "78701-1234", "7870", "78701 "]:
    print(f"{z!r:<13} valid ZIP: {bool(zip_code.fullmatch(z))}")

Output:

emails : ['ana.lopez@example.com', 'support@shop.co.uk']
phones : ['512-555-0147']
dates  : ['14/03/2025']
invoice: ('2025', '0042')
urls   : ['https://pythonguides.com/python-tutorials/']
'78701'       valid ZIP: True
'78701-1234'  valid ZIP: True
'7870'        valid ZIP: False
'78701 '      valid ZIP: False

These patterns are good for extracting data from text. For validation rules and their limits, see validate email addresses in Python, and for numbers extract numbers from a string.

Compile patterns you reuse

re.compile() returns a pattern object with the same methods (search, findall, …). The module functions also cache compiled patterns, but they look the pattern up on every call, so in a loop over many lines a compiled pattern is faster (compare the two timings below; your numbers will differ). Compiling also gives the pattern a name:

import re
import timeit

lines = [f"user{i}@example.com visited /page/{i}" for i in range(10_000)]
pattern = re.compile(r"/page/(\d+)")

ids = [int(m.group(1)) for line in lines if (m := pattern.search(line))]
print(len(ids), ids[:5])
print(pattern.pattern, pattern.flags == re.UNICODE)

t_compiled = timeit.timeit(lambda: [pattern.search(l) for l in lines], number=20)
t_module = timeit.timeit(lambda: [re.search(r"/page/(\d+)", l) for l in lines], number=20)
print(f"compiled: {t_compiled:.3f} s   re.search: {t_module:.3f} s")

Output:

10000 [0, 1, 2, 3, 4]
/page/(\d+) True
compiled: 0.032 s   re.search: 0.074 s

Common mistakes

import re

print(repr("\b"), repr(r"\b"))                          # "\b" is a backspace character
print(re.findall("\bcat\b", "cat catalog cat"))         # no raw string: nothing found
print(re.findall(r"\bcat\b", "cat catalog cat"))        # raw string: word boundaries

print(re.findall(r"3.14", "3.14 and 3514"))             # . matches any character
print(re.findall(r"3\.14", "3.14 and 3514"))            # escape it
print(re.escape("price (USD) $5.00?"))                  # escape user input automatically

m = re.search(r"\d+", "no digits here")
print(m)                                                # None, not a match object
try:
    m.group()
except AttributeError as e:
    print("AttributeError:", e)

try:
    re.compile(r"(unclosed")
except re.error as e:
    print("re.error:", e)

Output:

'\x08' '\\b'
[]
['cat', 'cat']
['3.14', '3514']
['3.14']
price\ \(USD\)\ \$5\.00\?
None
AttributeError: 'NoneType' object has no attribute 'group'
re.error: missing ), unterminated subpattern at position 0
Command Prompt output of common Python regex mistakes: missing raw string for \b, unescaped dot, re.escape, AttributeError on None match and re.error for an unclosed group
Raw strings, escaping, a None result and an invalid pattern.
  • Forgetting the r prefix: in a normal string, "\b" is a backspace. Always write patterns as raw strings. More in raw strings in Python.
  • Calling .group() on None: check if m: first, or use the walrus operator.
  • Using match() when you mean search(): match() only looks at the start.
  • Unescaped special characters: . ^ $ * + ? { } [ ] \ | ( ) need a backslash, or re.escape() for user input.
  • Regex for everything: str.startswith(), in, str.split() and str.replace() are simpler and faster for fixed text.

Next, try these string tutorials that use regex:

Frequently asked questions

What is a regular expression in Python?

A pattern that describes text, used with the built-in re module to search, validate, extract and replace parts of strings.

What is the difference between re.search() and re.match()?

re.match() only checks the beginning of the string; re.search() looks anywhere. Use re.fullmatch() to require the whole string to match.

How do I find all matches of a regex in Python?

re.findall(pattern, text) returns a list of matched strings; re.finditer() returns match objects with positions.

Why should I use raw strings for regex patterns?

Backslashes in normal strings are escape sequences ("\b" is a backspace). A raw string r"\b" passes the backslash to the regex engine.

How do I replace text with a regex?

Use re.sub(pattern, replacement, text). Reference groups with \1 or \g<name>, or pass a function.

How do I make a regex case-insensitive?

Pass re.IGNORECASE (or re.I) as the flags argument, or start the pattern with (?i).