Search
Python Regex Tutorial: re.search, findall, sub and Validation with Examples
Sooner or later every programmer needs to find a pattern inside text: pull the dates out of a log file, check if an email address looks valid, clean up extra spaces in a name, or turn a title into a URL slug. You could write loops and if statements for each case, but there is a much shorter way: regular expressions (regex). Python supports them through the built-in re module, so there is nothing to install.
In this tutorial you will learn the Python regex functions you will actually use: re.search(), re.match(), re.fullmatch(), re.findall(), re.finditer(), re.sub() and re.split(), plus groups, flags and the greedy vs lazy trap. Every example is a small script you can run, and the output shown below each one was copied from the terminal after running it on Python 3.13.
Requirements
- Python 3.8 or newer. The
remodule is part of the standard library. - A code editor such as VS Code, PyCharm or IDLE.
- Basic Python knowledge: strings, lists, loops and functions.
Regex Cheat Sheet: The Symbols You Need First
| Pattern | Meaning | Example match |
|---|---|---|
\d | Any digit 0-9 | 7 |
\w | Letter, digit or underscore | a, _ |
\s | Whitespace (space, tab, newline) | " " |
. | Any character except newline | x |
[abc] / [^abc] | One of a, b, c / anything except them | b |
+ * ? | One or more / zero or more / optional | \d+ matches 1042 |
{n} / {n,m} | Exactly n / between n and m times | \d{4} matches 2026 |
^ $ | Start / end of the string (or line) | |
( ) | Group: capture part of the match | |
| | OR | ERROR|WARNING |
One habit to start with: always write patterns as raw strings, like r"\d+". The r prefix stops Python from treating backslashes as escape characters before the regex engine even sees them.
Step 1: re.search(), re.match() and re.fullmatch()
These three functions return a match object when the pattern is found and None when it is not. The difference is where they look. Save this as 1_search_match.py:
- import re
- text = "Order #1042 was paid on 2026-11-05 by [email protected]"
- # re.search() scans the whole string and returns the FIRST match (or None)
- m = re.search(r"\d+", text)
- print("search:", m.group(), "at position", m.start())
- # re.match() only checks the BEGINNING of the string
- print("match 'Order':", re.match(r"Order", text))
- print("match digits:", re.match(r"\d+", text))
- # re.fullmatch() requires the WHOLE string to match the pattern
- print("fullmatch 1042:", bool(re.fullmatch(r"\d+", "1042")))
- print("fullmatch 1042a:", bool(re.fullmatch(r"\d+", "1042a")))
- # Always check for None before calling .group()
- m = re.search(r"Invoice", text)
- if m:
- print(m.group())
- else:
- print("No 'Invoice' found")
Run it with python 1_search_match.py. Output:
search: 1042 at position 7 match 'Order': <re.Match object; span=(0, 5), match='Order'> match digits: None fullmatch 1042: True fullmatch 1042a: False No 'Invoice' found
Remember it this way:
search()looks anywhere in the string. This is the one you will use most.match()only checks the start of the string. That is why\d+returnedNone: the text starts with "Order", not a number.fullmatch()requires the entire string to match. Use it for validation, so that "1042a" is rejected.
Calling .group() on None raises AttributeError: 'NoneType' object has no attribute 'group', one of the most common regex errors. Always check the result first, as the last part of the script does.
Step 2: Find All Matches and Capture Groups
re.findall() returns every match as a list. When the pattern contains groups, it returns only the captured parts. For the full match objects, use re.finditer(). Here we read a small log file:
- import re
- log = """2026-11-03 ERROR Database connection failed
- 2026-11-04 INFO User ana logged in
- 2026-11-05 ERROR Disk almost full
- 2026-11-05 WARNING Slow query (2.4s)"""
- # findall() returns every match as a list of strings
- print(re.findall(r"\d{4}-\d{2}-\d{2}", log))
- # With groups ( ), findall() returns tuples of the captured parts
- for date, level in re.findall(r"^(\S+) (ERROR|WARNING)", log, re.MULTILINE):
- print(date, "->", level)
- # Named groups make the code easier to read
- pattern = re.compile(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2}) (?P<level>[A-Z]+) (?P<message>.+)")
- for m in pattern.finditer(log):
- print(m.group("day"), m.group("level"), "|", m.group("message"))
- m = pattern.search(log)
- print(m.groupdict())
Run it with python 2_findall_groups.py. Output:
['2026-11-03', '2026-11-04', '2026-11-05', '2026-11-05']
2026-11-03 -> ERROR
2026-11-05 -> ERROR
2026-11-05 -> WARNING
03 ERROR | Database connection failed
04 INFO | User ana logged in
05 ERROR | Disk almost full
05 WARNING | Slow query (2.4s)
{'year': '2026', 'month': '11', 'day': '03', 'level': 'ERROR', 'message': 'Database connection failed'}
A few things happened here:
\d{4}-\d{2}-\d{2}found all four dates.- With two groups,
findall()returned (date, level) tuples, which we unpacked directly in theforloop. re.MULTILINEmakes^match at the start of every line, not just the start of the whole text.- Named groups
(?P<name>...)let you writem.group("level")instead ofm.group(4), andgroupdict()gives you a ready-made dictionary. re.compile()builds the pattern once so you can reuse it. Python caches recently used patterns anyway, but compiling makes the code cleaner when you use a pattern many times.
Step 3: Validate Email, Mobile Number, Student ID and Password
Form validation is the most popular use of regex in student projects. Combine fullmatch() with a pattern for each field:
- import re
- EMAIL = re.compile(r"[\w.+-]+@[\w-]+(\.[\w-]+)+")
- PH_MOBILE = re.compile(r"(\+63|0)9\d{9}")
- STUDENT_ID = re.compile(r"\d{4}-\d{5}")
- STRONG_PASSWORD = re.compile(r"(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}")
- tests = {
- "mobile": (PH_MOBILE, ["09171234567", "+639171234567", "0917123456", "9171234567"]),
- "student id": (STUDENT_ID, ["2026-00123", "2026-123", "26-00123"]),
- "password": (STRONG_PASSWORD, ["Passw0rd!", "password", "Short1!", "NoSymbol123"]),
- }
- for name, (pattern, values) in tests.items():
- print(f"--- {name}")
- for v in values:
- ok = pattern.fullmatch(v) is not None
- print(f"{v:<26} {'valid' if ok else 'INVALID'}")
Run it with python 3_validate.py. Output:
--- email [email protected] valid [email protected] valid carla@ INVALID dan@@mail.com INVALID --- mobile 09171234567 valid +639171234567 valid 0917123456 INVALID 9171234567 INVALID --- student id 2026-00123 valid 2026-123 INVALID 26-00123 INVALID --- password Passw0rd! valid password INVALID Short1! INVALID NoSymbol123 INVALID
How the patterns work:
- Email
[\w.+-]+@[\w-]+(\.[\w-]+)+: a name, one@, a domain, and at least one ".something" part. It is a practical check, not a full RFC validator. The only real proof that an email works is sending a confirmation message. - Philippine mobile
(\+63|0)9\d{9}: starts with+63or0, then 9, then nine more digits. - Student ID
\d{4}-\d{5}: four digits, a dash, five digits. Change the numbers to fit your school's format. - Strong password uses lookaheads
(?=...). Each one checks a rule (a lowercase letter, an uppercase letter, a digit, a symbol) without consuming characters, and.{8,}requires at least 8 characters.
Validate on the server too, not only with JavaScript in the browser, because browser checks are easy to bypass.
Step 4: Replace and Split Text with re.sub() and re.split()
re.sub(pattern, replacement, text) replaces every match. The replacement can refer to groups with \1, \2 and so on, or it can be a function that computes the new text:
- import re
- # re.sub() replaces every match
- messy = " Juan Dela Cruz "
- print(repr(re.sub(r"\s+", " ", messy).strip()))
- # Use groups in the replacement: change 11/05/2026 to 2026-11-05
- print(re.sub(r"(\d{2})/(\d{2})/(\d{4})", r"\3-\1-\2", "Due: 11/05/2026, Paid: 11/20/2026"))
- # Mask a mobile number except the last 4 digits
- print(re.sub(r"\d(?=\d{4})", "*", "09171234567"))
- # A function as the replacement: add 12% VAT to every price
- def add_vat(m):
- price = float(m.group(1))
- return f"P{price * 1.12:.2f}"
- print(re.sub(r"P(\d+(?:\.\d+)?)", add_vat, "Coffee P50, Bread P35.50, Milk P90"))
- # re.split() splits on a pattern, not just one character
- print(re.split(r"[,;|]\s*", "php, python;java| sql"))
- # Make a URL slug from a title
- title = "Python Regex Tutorial: re.search, findall & sub!"
- slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")
- print(slug)
Run it with python 4_sub_split.py. Output:
'Juan Dela Cruz' Due: 2026-11-05, Paid: 2026-11-20 *******4567 Coffee P56.00, Bread P39.76, Milk P100.80 ['php', 'python', 'java', 'sql'] python-regex-tutorial-re-search-findall-sub
Each line shows a real task:
\s+squeezes any run of spaces into one, cleaning names typed with extra spaces.r"\3-\1-\2"rearranges the captured month, day and year into the database-friendlyYYYY-MM-DDformat.\d(?=\d{4})matches a digit only if four more digits follow it, so the last four digits stay visible.- When the replacement is a function, Python calls it for every match, so you can do math with the matched value.
re.split(r"[,;|]\s*", ...)splits on commas, semicolons or pipes and removes the spaces after them in one step.
Step 5: Flags, Greedy vs Lazy, and re.escape()
The last script covers the details that cause the most confusing bugs:
- import re
- # re.IGNORECASE: match regardless of upper/lower case
- print(re.findall(r"python", "Python, PYTHON and python", re.IGNORECASE))
- # Greedy vs lazy: .* takes as much as possible, .*? as little as possible
- html = "<b>Name</b> and <b>Score</b>"
- print("greedy:", re.findall(r"<b>(.*)</b>", html))
- print("lazy: ", re.findall(r"<b>(.*?)</b>", html))
- # re.VERBOSE lets you write a long pattern on several lines with comments
- price = re.compile(r"""
- P # peso sign written as P
- (\d{1,3}(?:,\d{3})*) # whole number with optional thousands commas
- (?:\.(\d{2}))? # optional centavos
- """, re.VERBOSE)
- for m in price.finditer("Laptop P45,999.00, Mouse P350, Bag P1,250.75"):
- print(m.group(0), "->", m.group(1), m.group(2))
- # Escape user input before putting it inside a pattern
- user_search = "C++ (basics)"
- print(re.escape(user_search))
- print(bool(re.search(re.escape(user_search), "Intro to C++ (basics) course")))
Run it with python 5_flags_greedy.py. Output:
['Python', 'PYTHON', 'python'] greedy: ['Name</b> and <b>Score'] lazy: ['Name', 'Score'] P45,999.00 -> 45,999 00 P350 -> 350 None P1,250.75 -> 1,250 75 C\+\+\ \(basics\) True
re.IGNORECASE(orre.I) ignores letter case.- Greedy vs lazy:
.*grabs as much text as it can, so<b>(.*)</b>swallowed everything from the first<b>to the last</b>. Adding?(.*?) makes it stop at the first possible end. Note that for real HTML pages you should use a parser such as BeautifulSoup; regex is fine for small, predictable snippets. re.VERBOSElets you spread a long pattern over several lines and add comments. Spaces inside the pattern are ignored in this mode.re.escape()escapes special characters like+and(. Use it whenever you put user input inside a pattern; otherwise a search for "C++" raises an error or matches the wrong thing.
Common Python Regex Mistakes
- Forgetting the
rprefix."\b"in a normal string is a backspace character, whiler"\b"is a word boundary. - Using
match()when you meantsearch(). If your pattern "never matches", check this first. - Using
search()for validation.re.search(r"\d{4}", "abc12345xyz")succeeds. Usefullmatch()when the whole input must follow the format. - Not checking for
Nonebefore calling.group(). - Writing one giant pattern. If a regex becomes hard to read, split the job into smaller steps or use
re.VERBOSEwith comments.
Quick Reference
| Function | Returns | Use it to |
|---|---|---|
re.search(p, s) | First match or None | Find a pattern anywhere |
re.match(p, s) | Match at start or None | Check how a string begins |
re.fullmatch(p, s) | Match or None | Validate input |
re.findall(p, s) | List of strings/tuples | Extract all values |
re.finditer(p, s) | Iterator of match objects | Extract with positions and named groups |
re.sub(p, r, s) | New string | Replace, clean or reformat text |
re.split(p, s) | List of strings | Split on several separators |
Conclusion
Regular expressions look scary at first, but you only need a handful of symbols and seven functions to handle most real tasks: search() to find, fullmatch() to validate, findall() and finditer() to extract, and sub() and split() to transform text. Start with the examples above, change the patterns, and watch how the output changes. That is the fastest way to get comfortable with regex. You can download all five example scripts below.
Happy coding!