String methods like find(), replace() and split() are perfect when you know exactly what text you're looking for. But what if you want every phone number in a document, every price in a web page, or to check that a postcode looks valid? You don't know the exact text, only its shape: "three digits, a dash, four digits".
A regular expression, or regex, describes that shape. It's a small pattern language supported by almost every programming language, editor and database. In Python, regular expressions live in the built-in re module.
Regex has a reputation for looking cryptic, and long patterns can be. But a handful of symbols covers most everyday needs. In this lesson you'll learn those symbols, then use them to search, extract, replace and validate text, with patterns you can read and explain.
Your first patterns
import re
text = "Order 4521 shipped on 12 March"
match = re.search(r"\d+", text)
print(match.group()) # 4521- Write patterns as raw strings,
r"...", so backslashes reachreunchanged. re.search()finds the first match anywhere in the text, or returnsNone.match.group()gives the text that matched;match.start()gives its position.\dmeans "any digit" and+means "one or more", so\d+matches a whole number.
The building blocks
| Pattern | Matches |
|---|---|
\d / \D | A digit / anything except a digit |
\w | A letter, digit or underscore |
\s | Whitespace: space, tab, newline |
. | Any character except a newline |
[aeiou] / [A-Z] | One character from a set / range |
[^0-9] | Any character not in the set |
^ / $ | Start / end of the text |
\b | A word boundary |
Quantifiers say how many: * zero or more, + one or more, ? optional, {3} exactly three, {2,4} two to four. To match a special character literally, escape it: \. matches a real full stop.
search, match and fullmatch compared
The three matching functions differ only in where the pattern must match, and confusing them is a common source of bugs:
import re
code = "AB-1234"
print(bool(re.search(r"\d{4}", code))) # True: digits anywhere
print(bool(re.match(r"\d{4}", code))) # False: must start at position 0
print(bool(re.fullmatch(r"[A-Z]{2}-\d{4}", code))) # True: whole stringsearchscans the whole string for the first place the pattern fits.matchonly tries at the very beginning.fullmatchrequires the pattern to cover the entire string, which makes it the right tool for validating input.
Greedy and lazy quantifiers
By default, * and + are greedy: they match as much text as possible. That surprises people extracting text between markers:
html = "<b>bold</b> and <b>more</b>"
print(re.findall(r"<b>.*</b>", html)) # ['<b>bold</b> and <b>more</b>']
print(re.findall(r"<b>.*?</b>", html)) # ['<b>bold</b>', '<b>more</b>']Adding ? after a quantifier makes it lazy, matching as little as possible. For real HTML, use a parser such as BeautifulSoup rather than regex, but the greedy-versus-lazy idea applies to any text with repeated delimiters.
If you use the same pattern many times, re.compile() creates a pattern object with the same methods: postcode = re.compile(r"..."), then postcode.fullmatch(text). Python caches recently used patterns anyway, so compiling is mainly about readability and naming.
findall, sub and split
re.findall(pattern, text)returns a list of every match.re.sub(pattern, replacement, text)replaces every match, which is great for cleaning data.re.split(pattern, text)splits wherever the pattern matches, for example on commas or semicolons:re.split(r"[,;]\s*", text).re.fullmatch(pattern, text)succeeds only if the whole text matches, which is what you want for validation.
Add flags=re.IGNORECASE to any of them to ignore capital letters.
Groups
Round brackets create a group, which captures part of the match separately:
m = re.search(r"(\d{2})/(\d{2})/(\d{4})", "Due 05/03/2025")
day, month, year = m.groups()Name groups with (?P<name>...) and read them with m["name"], which makes long patterns easier to understand. In re.sub, refer to groups in the replacement with \1, \2 and so on, for example to reorder dates.
Python Regular Expressions Syntax
re functions
import re
re.search(r"pattern", text) # first match or None
re.fullmatch(r"pattern", text) # whole text must match
re.findall(r"pattern", text) # list of matches
re.sub(r"pattern", "new", text) # replace
re.split(r"pattern", text) # split
re.compile(r"pattern") # reuse a pattern
m.group() m.groups() m["name"]Python Regular Expressions Summary
- The
remodule provides regular expressions; write patterns as raw stringsr"...". \d,\w,\s,.and[...]match kinds of characters;+,*,?and{n}say how many.searchfinds the first match,findallall of them,fullmatchchecks the whole text.subreplaces matches andsplitsplits on a pattern.- Brackets create groups, which can be named and reused in replacements.
Python Regular Expressions Examples
Each example shows the code, the output it produces and what each part does.
Extracting prices
import re
page = "Tea £2.50, Cake £3.95, Water £1, Gift card £20.00"
prices = re.findall(r"£(\d+(?:\.\d{2})?)", page)
print(prices)
print("Total:", sum(float(p) for p in prices))['2.50', '3.95', '1', '20.00']
Total: 27.45Line-by-line explanation
- Line 4£ then one or more digits, then optionally a dot and two digits. (?:...) groups without capturing, so findall returns only the outer group: the number.
- Line 6The matches are strings, so convert them to floats before adding.
Validating a username
import re
pattern = r"[a-z][a-z0-9_]{2,14}"
for name in ["sam_lee", "7ninja", "ab", "codevra_fan_2025"]:
if re.fullmatch(pattern, name):
print(f"{name}: ok")
else:
print(f"{name}: invalid")sam_lee: ok
7ninja: invalid
ab: invalid
codevra_fan_2025: invalidLine-by-line explanation
- Line 3A lowercase letter, then 2 to 14 lowercase letters, digits or underscores: 3 to 15 characters in total.
- Line 5fullmatch checks the entire name, not just part of it. The last name is 16 characters, one too many.
Cleaning and reformatting
import re
messy = "Call 07700 900123 or 07700-900456 today"
clean = re.sub(r"\s+", " ", messy)
print(clean)
dates = "Start 2025-03-05, end 2025-04-30"
print(re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\3/\2/\1", dates))Call 07700 900123 or 07700-900456 today
Start 05/03/2025, end 30/04/2025Line-by-line explanation
- Line 4Any run of whitespace becomes a single space.
- Line 8Three groups capture year, month and day, and the replacement puts them in a new order.
Common Python Regular Expressions Mistakes
Forgetting the r prefix
re.search("\bcat\b", text) # \b becomes a backspace characterre.search(r"\bcat\b", text)Using search to validate
re.search(r"\d{5}", "abc123456xyz") # matches, but isn't a 5-digit codere.fullmatch(r"\d{5}", "abc123456xyz") # NoneNot checking for None
m = re.search(r"\d+", "no numbers")
print(m.group()) # AttributeError: 'NoneType'm = re.search(r"\d+", "no numbers")
if m:
print(m.group())Python Regular Expressions in Real Projects
Regex in real work
Developers use regex to pull values out of log files, validate form fields, rename hundreds of files, clean up scraped data and search code in their editor. Data analysts use the same patterns in pandas and SQL. Learning a small set of symbols pays off for years, because the syntax is almost identical across languages.
Python Regular Expressions FAQ
What is the difference between re.search() and re.match()?
re.search() looks for the pattern anywhere in the string. re.match() only checks whether the string starts with it. For validating a whole value, use re.fullmatch(), which requires the pattern to match from the first character to the last.
Why should regex patterns be raw strings?
In a normal string, Python treats backslashes as escape characters first, so "\b" becomes a backspace character before re ever sees it. A raw string, r"\b", passes the backslash through unchanged. Using r"..." for every pattern avoids this whole class of bugs.
Can I validate an email address with a regex?
You can check that it has a plausible shape, such as r"[^@\s]+@[^@\s]+\.[^@\s]+", which catches obvious typos. A pattern that accepts every valid address and rejects every invalid one is extremely complicated. The only real proof an address works is sending a confirmation email to it.
How do I make a regex ignore upper and lower case?
Pass flags=re.IGNORECASE (or re.I) to the function, for example re.findall(r"python", text, flags=re.I). You can also put (?i) at the very start of the pattern itself.
How do I match a literal dot, bracket or question mark?
Escape it with a backslash: \., \(, \?. Inside square brackets most special characters lose their meaning, so [.?] matches a dot or a question mark. re.escape(text) escapes every special character in a string for you, which is useful when part of the pattern comes from user input.
Python Regular Expressions Exercises
Answer each one and check it instantly. If you get stuck, open the hint before the answer.
Complete the pattern so it finds all whole numbers in the text.
re.findall(r"\d", "3 cats and 12 dogs")
What does this print?
import re
print(re.findall(r"[aeiou]", "regex"))
What does this print?
import re
print(re.sub(r"\d", "#", "PIN 4821"))
Which pattern matches exactly a UK-style time like 09:45?
Why use re.fullmatch for validation instead of re.search?
Python Regular Expressions Quiz
5 questions to check what you learned.
Which module provides regular expressions in Python?
- regex
- re
- string
- pattern
What does
?mean after a character in a pattern?- One or more
- Zero or more
- Optional: zero or one
- Exactly one
What does
re.searchreturn when nothing matches?- An empty string
- False
- None
- An error
What does
\smatch?- A whitespace character
- The letter s
- Any symbol
- The start of the text
In
re.sub(r"(\w+) (\w+)", r"\2 \1", "Ada Lovelace"), what's the result?- Ada Lovelace
- \2 \1
- AdaLovelace
- Lovelace Ada