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']
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']
re function on the same text.| Function | Returns | Use it to |
|---|---|---|
re.search(p, s) | Match or None | Find the first match anywhere |
re.match(p, s) | Match or None | Match at the start of the string only |
re.fullmatch(p, s) | Match or None | Validate: 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 objects | Every match with positions, memory friendly |
re.sub(p, repl, s) | New string | Replace matches |
re.subn(p, repl, s) | (new string, count) | Replace and count |
re.split(p, s) | List of strings | Split on a pattern |
re.compile(p) | Pattern object | Reuse a pattern |
Splitting deserves its own guide: see how to split a string with regex in Python.
Regex syntax cheat sheet
| Pattern | Matches |
|---|---|
. | Any character except a newline |
\d / \D | A digit / a non-digit |
\w / \W | A word character (letter, digit, _) / anything else |
\s / \S | Whitespace (space, tab, newline) / non-whitespace |
\b | A 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|b | a 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']
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')]
^, . 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
None result and an invalid pattern.- Forgetting the
rprefix: in a normal string,"\b"is a backspace. Always write patterns as raw strings. More in raw strings in Python. - Calling
.group()onNone: checkif m:first, or use the walrus operator. - Using
match()when you meansearch():match()only looks at the start. - Unescaped special characters:
. ^ $ * + ? { } [ ] \ | ( )need a backslash, orre.escape()for user input. - Regex for everything:
str.startswith(),in,str.split()andstr.replace()are simpler and faster for fixed text.
Next, try these string tutorials that use regex:
- Split a string with regex in Python
- Validate email addresses in Python
- Split strings with multiple delimiters
- Check if a string starts with a substring
- Extract numbers from a string
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).
Bijay Kumar is a 13-time Microsoft MVP with more than 18 years in software development, and the founder of Python Guides and TSinfo Technologies. He started out building .NET and SharePoint solutions at HP, TCS and KPIT before moving into Python, machine learning and AI, and he also builds web apps with TypeScript and React. He writes the tutorials here himself, and every example is run before publishing so you see the real output. More about Bijay · Microsoft MVP profile · LinkedIn