These python regex examples put regular expressions to work on real jobs: validate emails, phone numbers, and passwords, parse log files and CSV (comma-separated values) data, pull clean information out of messy text, and wire it all into a small text processing pipeline. Every pattern here is tested and ready to paste into your own code.
“Some people, when faced with a problem, think: I know, I will use regular expressions. Now they have two problems. Use the right tool, and the right amount of it.”
Jamie Zawinski (the famous warning), with the practical reply most of us learn the hard way
Last Updated: July 2026 | Tested on: Python 3.14.6 | Difficulty: Intermediate | Reading Time: 18 minutes
The regular expressions tutorial handed you the regex toolbox: the symbols, groups, and lookaheads. This one is where you actually build something with those tools. Think of it like the difference between owning a set of wrenches and fixing an actual leaking tap. You will validate what users type, read through server logs, pull links out of paragraphs, and scrub messy strings into clean ones.
Here is the one habit that separates people who fear regex from people who use it every day. Do not try to write one giant pattern that does everything. Build small patterns, test each one, then stack them like LEGO bricks. A bouncer at a club does not check your age, your ticket, and your bag all in a single glance. He checks one thing, then the next, then the next. Every recipe below follows the same idea: one small, testable pattern at a time.
Tip: click the diagram to open it full screen, then use the zoom and pan controls for a closer look.
The diagram shows a regex validation pipeline for emails, phone numbers, and URLs. Raw input goes in at the top, then passes through a row of small checks, where each step confirms one thing such as format, length, or which characters are allowed, before the value is accepted or rejected. This is how real apps validate user input: they stack a few simple patterns instead of writing one giant regex that nobody can read later. Every one of the python regex examples in this post follows that same step by step shape.
Table of Contents
Python Regex Examples for Validation
Email Validation
You want to reject obvious junk before it reaches your database: an address with no domain, no name before the @, or a space in the middle. Think of it like sorting post: an envelope needs a name, a building, and a city written in the right order, or it goes straight back. The pattern below catches the everyday mistakes. It is not the full email standard (more on why that is fine in a second), but it is the version you will actually ship.
📄 validate_email.py: good enough for 99% of real-world emails
import re
def validate_email(email):
"""Validate email format. Not full RFC 5322, but practical."""
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
return bool(re.match(pattern, email))
test_emails = [
"rahul@technoscripts.com", # Valid
"aditi.sharma@gmail.com", # Valid
"viraj+tag@company.co.in", # Valid
"invalid@", # Invalid: no domain
"@missing.com", # Invalid: no name before the @
"spaces in@email.com", # Invalid: has a space
]
for email in test_emails:
status = "VALID" if validate_email(email) else "INVALID"
print(f"{status:>7}: {email}")
▶ Output
VALID: rahul@technoscripts.com VALID: aditi.sharma@gmail.com VALID: viraj+tag@company.co.in INVALID: invalid@ INVALID: @missing.com INVALID: spaces in@email.com
What happened here: The pattern reads as three blocks. First, one or more letters, digits, or the symbols ._%+- (the name part). Then a literal @. Then the domain, ending in a dot and at least two letters. The ^ and $ anchors say “match the whole string, start to finish”, so a stray space anywhere kills the match. That is why spaces in@email.com fails. One honest warning: no regex can perfectly match every legal email address (the official rules are wild), so for anything important, validate the format like this, then send a confirmation link and let the user prove the inbox is real.
Password Strength
A password form usually has a checklist: at least 8 characters, one uppercase, one lowercase, a digit, a special symbol. You could write one monster pattern, but then a failing password only tells you “nope” without saying which rule broke. The trick here is to keep each rule as its own tiny pattern, so you can show the user exactly what is missing. Below, a new user named Pravin signs up with Pravin@2026 and we score it rule by rule.
📄 validate_password.py: one small lookahead per rule
import re
def check_password(password):
"""Check password meets strength requirements."""
checks = {
"At least 8 characters": r".{8,}",
"Contains uppercase": r"(?=.*[A-Z])",
"Contains lowercase": r"(?=.*[a-z])",
"Contains digit": r"(?=.*\d)",
"Contains special char": r"(?=.*[!@#$%^&*()_+=-])",
}
results = {}
for rule, pattern in checks.items():
results[rule] = bool(re.search(pattern, password))
return results
password = "Pravin@2026"
for rule, passed in check_password(password).items():
icon = "PASS" if passed else "FAIL"
print(f" [{icon}] {rule}")
▶ Output
[PASS] At least 8 characters [PASS] Contains uppercase [PASS] Contains lowercase [PASS] Contains digit [PASS] Contains special char
What happened here: Each rule is a lookahead, written (?=...). A lookahead just asks “is this thing somewhere ahead of me?” without actually consuming any characters, so all five checks can scan the same password independently. (?=.*[A-Z]) means “somewhere later there is an uppercase letter”. Because we test each rule on its own with a dictionary, the result tells you which boxes are ticked and which are not. Pravin@2026 ticks all five. Swap in pravin and the uppercase, digit, and special-char rows would flip to FAIL, and you could show that to the user word for word.
Indian Phone Number
Real people type phone numbers in a dozen ways: with +91, with a leading 0, with spaces or dashes, or just ten bare digits. Instead of trying to match every messy layout in one pattern, clean the input first, then check the clean version. It is like wiping a dirty whiteboard before you read what is written on it.
📄 validate_phone.py: clean first, then check
import re
def validate_indian_phone(phone):
"""Validate Indian mobile numbers in various formats."""
pattern = r"^(?:\+91[-.\s]?|91[-.\s]?|0)?[6-9]\d{9}$"
cleaned = re.sub(r"[\s()-]", "", phone)
return bool(re.match(pattern, cleaned))
phones = [
"+91 9876543210", # Valid
"91-9876543210", # Valid
"09876543210", # Valid
"9876543210", # Valid
"1234567890", # Invalid: does not start with 6 to 9
"+91 12345", # Invalid: too short
]
for phone in phones:
status = "VALID" if validate_indian_phone(phone) else "INVALID"
print(f"{status:>7}: {phone}")
▶ Output
VALID: +91 9876543210 VALID: 91-9876543210 VALID: 09876543210 VALID: 9876543210 INVALID: 1234567890 INVALID: +91 12345
What happened here: The re.sub(r"[\s()-]", "", phone) line strips out spaces, brackets, and dashes, so every input arrives at the check as plain digits (maybe with a +91 or 0 in front). The pattern then allows an optional country code, and the real test is [6-9]\d{9}: an Indian mobile number is exactly ten digits and starts with 6, 7, 8, or 9. That is why 1234567890 is rejected (it starts with 1) and +91 12345 is rejected (too short). Cleaning first turned six different-looking inputs into one simple rule.
Parsing Patterns
Log File Parser
A server log is a wall of text, but every line follows the same shape: a timestamp, a level, a module in brackets, then a message. When the shape is fixed, regex shines. Named groups let you label each piece, so instead of counting characters by hand you pull out fields by name, like reading values off a form. The Python regex examples in this section switch from validating input to extracting data out of it. In the sample log below, a user named Aviraj keeps failing to log in, and our parser catches it.
📄 parse_logs.py: extract structured data from server logs
import re
from collections import Counter
log_lines = """
2026-03-27 14:30:22 ERROR [auth] Failed login for user=aviraj ip=192.168.1.50
2026-03-27 14:30:25 INFO [api] GET /users/42 status=200 time=45ms
2026-03-27 14:31:05 WARNING [db] Slow query time=3200ms table=orders
2026-03-27 14:31:18 ERROR [auth] Failed login for user=aviraj ip=192.168.1.50
2026-03-27 14:32:01 INFO [api] POST /orders status=201 time=120ms
""".strip().split("\n")
# Pattern with named groups
pattern = re.compile(
r"(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+"
r"(?P<level>\w+)\s+"
r"\[(?P<module>\w+)\]\s+"
r"(?P<message>.+)"
)
level_counts = Counter()
for line in log_lines:
match = pattern.match(line)
if match:
data = match.groupdict()
level_counts[data["level"]] += 1
if data["level"] == "ERROR":
print(f"ALERT: {data['timestamp']} [{data['module']}] {data['message']}")
print(f"\nSummary: {dict(level_counts)}")
▶ Output
ALERT: 2026-03-27 14:30:22 [auth] Failed login for user=aviraj ip=192.168.1.50
ALERT: 2026-03-27 14:31:18 [auth] Failed login for user=aviraj ip=192.168.1.50
Summary: {'ERROR': 2, 'INFO': 2, 'WARNING': 1}
What happened here: The pattern uses (?P<timestamp>...) style named groups, so each captured chunk gets a name. After a line matches, match.groupdict() hands you a plain dictionary like {'timestamp': ..., 'level': 'ERROR', 'module': 'auth', 'message': ...}. From there it is ordinary Python: we count how many lines hit each level with a Counter, and we print an alert whenever the level is ERROR. Notice we compiled the pattern once with re.compile() before the loop. On a real log with millions of lines, that one move saves a lot of work, because Python does not rebuild the pattern on every line.
Data Cleaning with re.sub()
Data that comes from humans is messy: stray HTML tags, double spaces, phone numbers written three different ways, and the dreaded triple exclamation mark. re.sub() is your eraser and pen in one. It finds a pattern and writes something else in its place. Chain a few small re.sub() calls and a grimy string comes out clean on the other side. The sample below is a scribbled call note about a customer named Prathamesh, complete with stray HTML and a dot-separated phone number.
📄 data_cleaning.py: normalize messy user input
import re
def clean_text(text):
"""Normalize messy text data."""
# Remove extra whitespace
text = re.sub(r"\s+", " ", text.strip())
# Normalize phone separators to hyphens
text = re.sub(r"(\d{3})[.\s](\d{3})[.\s](\d{4})", r"\1-\2-\3", text)
# Remove HTML tags
text = re.sub(r"<[^>]+>", "", text)
# Fix double punctuation
text = re.sub(r"([.!?])\1+", r"\1", text)
return text
messy = " <b>Prathamesh</b> called at 987.654.3210 today!! "
print(f"Before: '{messy}'")
print(f"After: '{clean_text(messy)}'")
▶ Output
Before: ' <b>Prathamesh</b> called at 987.654.3210 today!! ' After: 'Prathamesh called at 987-654-3210 today!'
What happened here: Four small re.sub() calls run in order, and each one fixes one thing. \s+ squashes any run of spaces into a single space. The phone pattern captures three digit groups and rewrites them with hyphens using backreferences (\1, \2, \3, which mean “the first group you captured, the second, the third”). The HTML pattern deletes anything inside angle brackets. The last one collapses repeated ., !, or ? down to one. Stacking small replacements like this is far easier to read and debug than one enormous pattern, and you can comment each line so the next person knows what it does.
Text Processing Patterns
Say you have a block of text, maybe a chat message or a README, and you want just the web links. re.findall() grabs every match at once and hands them back as a list. It works like running a magnet over a drawer of mixed stationery: only the pins jump out, everything else stays put. Perfect for “find all of X” jobs.
📄 extract_urls.py: pull URLs from any text
import re
text = """
Check out https://technoscripts.com for tutorials.
Also visit http://docs.python.org/3/library/re.html for regex docs.
Contact us at vinay@technoscripts.com (not a URL).
FTP at ftp://files.example.com/data.zip
"""
# Extract HTTP/HTTPS URLs
url_pattern = r"https?://[^\s,)\"']+"
urls = re.findall(url_pattern, text)
for url in urls:
print(f"Found: {url}")
▶ Output
Found: https://technoscripts.com Found: http://docs.python.org/3/library/re.html
What happened here: The pattern https?://[^\s,)\"']+ means “http with an optional s, then ://, then keep grabbing characters until you hit a space, comma, closing bracket, or quote”. That stop list keeps the link from swallowing the punctuation around it. The ftp:// link is skipped on purpose, because we only asked for http and https. And the email address is left alone, since it has no :// in it. findall simply returns the two links it found, ready to loop over.
Now a trickier one. Take an employee record for a developer named Rahul Mahadik. A CSV line looks easy to split on commas, until a value like "Pune, Maharashtra" has a comma inside the quotes. A plain text.split(",") would tear that city in half. Regex can tell “comma inside quotes” apart from “comma between fields”.
📄 csv_parsing.py: handle quoted fields that contain commas
import re
# Simple str.split(",") fails on quoted fields with commas
csv_line = 'Rahul Mahadik,28,"Pune, Maharashtra",Senior Developer'
# Regex: match quoted strings OR non-comma sequences
fields = re.findall(r'"([^"]+)"|([^,]+)', csv_line)
parsed = [quoted or unquoted for quoted, unquoted in fields]
print(parsed)
▶ Output
['Rahul Mahadik', '28', 'Pune, Maharashtra', 'Senior Developer']
What happened here: The pattern has two options separated by | (which means “or”). The left side, "([^"]+)", matches a quoted chunk and captures what is inside the quotes. The right side, ([^,]+), matches a run of characters that are not commas. For each match, exactly one of the two groups has text and the other is empty, so quoted or unquoted picks the one that actually matched. That is how Pune, Maharashtra survives as a single field. One honest note: for real CSV files, reach for Python’s built-in csv module first. It handles escaped quotes and edge cases this pattern does not. This example is here to show the technique, not to replace the right tool.
When NOT to Use Regex
Regex is powerful, and that is exactly why people overuse it. Half of becoming good with regex is knowing when to put it down, which is why any honest set of python regex examples has to include the cases where the answer is: skip the regex. Reaching for regex to check a simple substring is like firing up a chainsaw to slice a tomato: it works, but a knife is faster and nobody gets hurt. If a plain string method does the job, it will be faster to write, easier to read, and harder to get wrong.
In the example below we just want to know if a message greets your friend Niranjan. Here are the jobs where regex is the wrong tool.
📄 when_not_regex.py: simpler alternatives exist
# DON'T use regex for simple string checks text = "Hello, Niranjan" # Bad: regex for simple containment import re if re.search(r"Niranjan", text): pass # Good: use the 'in' operator if "Niranjan" in text: pass # DON'T parse HTML with regex. Use BeautifulSoup # DON'T parse JSON with regex. Use the json module # DON'T chase a "perfect" email regex. Use a library or send a verification email # DON'T write regex longer than ~80 chars without re.VERBOSE and comments
What happened here: Both checks find the name “Niranjan” in the text, but "Niranjan" in text reads like plain English and runs faster, while re.search drags in the whole regex engine for nothing. The bottom comments point at the classic traps. HTML and JSON have nested structure that regex cannot reliably handle, so use a real parser. Email rules are so complex that no practical regex is truly complete, so confirm with a real message. And once a pattern grows past about 80 characters, switch on re.VERBOSE so you can space it out and add comments, otherwise future-you will not be able to read it.
Common Mistakes
This first one is the scariest because the pattern looks harmless. Nesting one quantifier inside another, like (a+)+, makes the engine try a wildly growing number of ways to match a string that will never match. It is like a delivery rider trying every possible route through a city to reach an address that does not exist: each extra street doubles the routes to check. The time roughly doubles with each extra character, so it starts feeling instant and then suddenly hangs your program.
❌ Mistake: catastrophic backtracking
import re # BAD: nested quantifiers (a+)+ blow up on input that never matches. # On Python 3.14.6, re.match(r"(a+)+b", "a" * 25) takes about 2 seconds, # and the time roughly DOUBLES per extra "a". A few more characters # means minutes, then hours. The string never matches (no trailing b), # so the engine keeps trying every combination before giving up. # GOOD: flatten it. Same logic, no explosion. # re.match(r"a+b", "a" * 25) # instant # Rule of thumb: never nest quantifiers like (a+)+ or (a*)*
What happened here: There is nothing to print, because the bad line is left commented on purpose. If you uncomment it, your script will appear to freeze. The fix is almost always to flatten the pattern: a+b does the exact same job as (a+)+b without the explosion. The lesson is short. When you see one quantifier wrapped inside another, stop and rewrite it.
This one bites almost everyone once. The moment your pattern has a capturing group in it, findall quietly changes what it returns: you get the group contents, not the whole match. People expect $50 and get 50, then waste an hour wondering where the dollar sign went.
❌ Mistake: findall with groups returns only the groups
import re
# When the pattern has a group, findall returns GROUP contents, not the full match
text = "Prices: $50, $120, $8"
# Surprise: you get the digits inside the group, without the $
print(re.findall(r"\$(\d+)", text)) # ['50', '120', '8'], just the digits
# Want the full match AND the group? Use finditer
for m in re.finditer(r"\$(\d+)", text):
print(f"Full: {m.group(0)}, Amount: {m.group(1)}")
▶ Output
['50', '120', '8'] Full: $50, Amount: 50 Full: $120, Amount: 120 Full: $8, Amount: 8
What happened here: The first line proves the catch: findall returned only the digits, because the (\d+) group told it “this is the part I care about”. When you need both the full match and the captured piece, switch to finditer. It hands you a match object for each hit, where m.group(0) is the whole match ($50) and m.group(1) is the first group (50). Same pattern, but now you keep the dollar sign too.
Wrapping Up
You now have a working set of python regex examples for the jobs that actually come up: validating emails, passwords, and Indian phone numbers, parsing logs with named groups, cleaning messy text with chained re.sub() calls, and pulling URLs and CSV fields out of tricky strings. Just as important, you know when to skip regex entirely and how to spot the two classic traps: nested quantifiers and findall with capturing groups. The habit to keep is the one from the top of the post: small patterns, tested one at a time, stacked like LEGO bricks.
Next up is the collections module, where tools like Counter (which you already met in the log parser) make counting and grouping almost effortless. For every post in order, from beginner basics to AI/ML, visit the Python + AI/ML tutorial series home.
Frequently Asked Questions
How do I validate an email address with Python regex?
Use r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$" for practical validation. No regex can perfectly match every legal address (the official RFC 5322 rules are huge), so in production combine this format check with actually sending a verification email. These python regex examples are tested on Python 3.14.6.
How do I extract data from log files with regex?
Use re.compile() with named groups (?P<name>...) for structured extraction. Compile the pattern once, then call pattern.match(line) in a loop. Use match.groupdict() to get a dictionary of named captures per line.
How do I clean text data with Python regex?
Chain re.sub() calls: remove HTML tags with r"<[^>]+>", normalize whitespace with r"\s+", fix punctuation with r"([.!?])\1+". Use backreferences (\1) to keep the matched content during replacement.
When should I NOT use regex?
Skip regex for: simple string checks (use in or startswith()), parsing HTML or XML (use BeautifulSoup or lxml), parsing JSON (use the json module), and complex grammars (use a real parser). If a built-in string method can do it, prefer that.
What is catastrophic backtracking?
Nested quantifiers like (a+)+ make the regex engine try a huge, fast-growing number of paths on input that never matches. On Python 3.14.6, a 25-character string takes a couple of seconds, and the time roughly doubles per extra character, so a few more characters means minutes. Avoid nested quantifiers, or use atomic groups and possessive quantifiers.
Try It Yourself
Your turn. Write a parse_address() function that pulls the street, city, state, and pincode out of Indian addresses like “42, MG Road, Pune, Maharashtra 411001”. Use named groups so you can read the pieces back by name, and follow the habit from this post: build it one small pattern at a time, test each piece, then handle the messy cases like a missing comma or extra spaces. If you get stuck, the python regex examples above already contain every technique you need: named groups, cleaning with re.sub(), and one rule per pattern.
Interview Questions on Python Regex
Try each one aloud first. The phrasing you produce under mild pressure is what interviews measure.
Q: What is the difference between re.match(), re.search(), and re.fullmatch(), and which one should you use for validation?
re.match() only checks from the start of the string, re.search() scans the whole string for the first hit anywhere, and re.fullmatch() requires the entire string to match. For validating user input like an email or phone number, re.fullmatch() is the safest choice because you cannot forget the anchors. A pattern with re.match() but no $ at the end would happily accept valid@email.com trailing garbage.
Q: Your email validator uses re.match() with a pattern ending in $, yet the string “user@example.com\n” passes. Why, and how do you fix it?
In Python regex, $ matches at the end of the string but also just before a trailing newline, so "user@example.com\n" slips through and the newline ends up in your database. Fix it by using \Z instead of $ (it matches only at the true end of the string), by switching to re.fullmatch(), or by calling .strip() on the input before validating. This exact bug shows up a lot with input read from files or web forms.
Q: You run re.findall() on a 2 GB log file loaded into one string and memory usage spikes until the process is killed. What do you change?
Two things. First, stop loading the whole file: iterate over it line by line, since each log entry is one line anyway. Second, if you must scan a large text in one go, use re.finditer() instead of re.findall(): it yields match objects one at a time instead of building the entire result list in memory. Also compile the pattern once with re.compile() outside the loop so it is not looked up on every line.
Q: Why do we write regex patterns as raw strings like r”\d+” in Python?
Backslashes mean something to both Python and the regex engine, so without the r prefix you would have to escape them twice: "\\d+" instead of r"\d+". Raw strings tell Python to pass backslashes through untouched, so what you write is exactly what the regex engine sees. It also avoids silent bugs: "\b" is a backspace character to Python, while r"\b" is the word-boundary token you actually wanted.
Q: A teammate removes HTML tags with re.sub(r”<.*>”, “”, text) and entire sentences disappear. What went wrong?
.* is greedy: it grabs as much as it can, so in <b>hello</b> it matches from the first < all the way to the last >, deleting the text in between. The fix is either the lazy version <.*?> or, better, the negated class <[^>]+> used in this post, which cannot cross a closing bracket. For real HTML documents, hand the job to a parser like BeautifulSoup instead.
Q: How do backreferences work in re.sub(), and when would you use named groups there?
In the replacement string, \1, \2, and so on paste back whatever the corresponding capturing group matched, which is how the phone cleaner rewrote 987.654.3210 as 987-654-3210. With named groups you write \g<name> in the replacement, for example re.sub(r"(?P<area>\d{3})", r"(\g<area>)", text). Named groups earn their keep in long patterns where \3 tells a reader nothing but \g<pincode> documents itself.
Further reading: for the full reference, see Python re module documentation.
Related Posts
Previous: Python: Regular Expressions, Patterns, Groups, Lookaheads
Next: Python: Collections Module (Counter, defaultdict, deque, namedtuple)
Series Home: Python + AI/ML Tutorial Series

No comment