Exercises: Regular Expressions
These exercises progress from basic pattern matching to complex extraction and transformation tasks. All exercises use Python 3.12+ and the re module.
Difficulty Guide: - ⭐ Foundational (5-10 min each) - ⭐⭐ Intermediate (10-20 min each) - ⭐⭐⭐ Challenging (20-40 min each) - ⭐⭐⭐⭐ Advanced/Research (40+ min each)
Part A: Pattern Reading ⭐
A.1. For each regex pattern below, describe in plain English what it matches, then list two strings that would match and one that would not.
a) r"\d{3}-\d{4}"
b) r"^[A-Z][a-z]+$"
c) r"\b\w{5}\b"
d) r"[aeiou]+"
e) r"https?://\S+"
A.2. What is the difference between re.search(), re.match(), and re.fullmatch()? For the string "Hello, World!" and pattern r"World", what does each function return?
A.3. Why should you use raw strings (r"...") for regex patterns? What goes wrong with re.search("\bcat\b", "the cat sat") versus re.search(r"\bcat\b", "the cat sat")?
A.4. Explain the difference between [^abc] and ^[abc]. Give an example string and show what each matches.
A.5. What does each of these quantifiers mean? Give a concrete example of a match for each.
a) +
b) *
c) ?
d) {3}
e) {2,5}
Part B: Pattern Writing ⭐⭐
B.1. Write a regex pattern that matches a US zip code — either the 5-digit form (12345) or the ZIP+4 form (12345-6789). Test it against these inputs:
test_zips = ["12345", "12345-6789", "1234", "123456", "12345-678", "ABCDE"]
B.2. Write a regex pattern that matches a time in 12-hour format like 2:30 PM or 11:45 am. The hours should be 1-12, minutes 00-59, and the AM/PM part should be case-insensitive. Test your pattern with re.findall() on this string:
text = "Meet at 2:30 PM, not 13:00 or 9:60 AM. Lunch at 12:00 pm."
B.3. Write a regex pattern using re.findall() to extract all hashtags (words starting with #) from a social media post:
post = "Just finished #Python chapter 22! #regex #PatternMatching is powerful. Not a #1 priority though."
Expected output: ['#Python', '#regex', '#PatternMatching'] (Note: #1 should not match because it's followed by a space and a digit, not a word.)
B.4. Dr. Patel has a file where gene identifiers follow the format: 2-4 uppercase letters, followed by a digit, optionally followed by up to 3 more uppercase letters or digits. Examples: TP53, BRCA1, MYC7, DNMT3A. Write a regex that matches these identifiers and test it with:
text = "Genes of interest: TP53, BRCA1, DNMT3A, abc123, X, MYC7, TOOLONG12345"
B.5. Elena receives donation amounts in various formats. Write a regex that matches dollar amounts like $500`, `$1,234.56, $0.99`, and `$10,000. Use re.findall() to extract all amounts from:
text = "Donations: $500, $1,234.56, and $10,000. Not $abc or just 500."
Part C: Extraction and Groups ⭐⭐-⭐⭐⭐
C.1. Given this list of log entries, use a regex with named capture groups to extract the timestamp, log level, and message from each:
logs = [
"2025-03-14 08:23:17 ERROR Database connection timeout",
"2025-03-14 08:24:01 INFO Recovery initiated",
"2025-03-14 08:25:33 WARNING Disk space at 10%",
]
Print each as a dictionary with keys timestamp, level, and message.
C.2. Write a function extract_urls(text: str) -> list[dict] that finds all URLs in a string and returns each as a dictionary with keys protocol, domain, and path:
text = "Visit https://example.com/about or http://docs.python.org/3/library/re.html"
Expected output:
[
{"protocol": "https", "domain": "example.com", "path": "/about"},
{"protocol": "http", "domain": "docs.python.org", "path": "/3/library/re.html"},
]
C.3. Write a function reformat_names(text: str) -> str that converts names from "Last, First" format to "First Last" format using re.sub() with group references:
roster = "Patel, Anika\nVasquez, Elena\nSmith, Jordan"
print(reformat_names(roster))
# Anika Patel
# Elena Vasquez
# Jordan Smith
C.4. Write a function mask_credit_cards(text: str) -> str that finds credit card numbers (four groups of four digits separated by spaces or dashes) and replaces all but the last four digits with X:
text = "Card: 4111-1111-1111-1234 and 5500 0000 0000 5678"
print(mask_credit_cards(text))
# Card: XXXX-XXXX-XXXX-1234 and XXXX XXXX XXXX 5678
Hint: use a function as the replacement argument to re.sub().
Part D: Greedy vs. Lazy and Edge Cases ⭐⭐⭐
D.1. Explain what happens with each of these patterns applied to the string '<a>link1</a> text <a>link2</a>':
a) r"<a>.*</a>"
b) r"<a>.*?</a>"
What does each return with re.findall()? Why?
D.2. Write a regex that extracts the contents of all quoted strings (using double quotes) from:
text = 'She said "hello" and then "goodbye" before leaving.'
Your result should be ['hello', 'goodbye']. Explain why greedy matching would fail.
D.3. A student writes this pattern to validate an email: r".+@.+". Explain at least three problems with this pattern. Write an improved (but still simple) version and test it against these inputs:
tests = [
"user@example.com", # valid
"@example.com", # invalid — no username
"user@", # invalid — no domain
"user@exam ple.com", # invalid — space in domain
"user.name@sub.domain.org", # valid
"", # invalid — empty
]
Part M: Mixed Practice (Interleaved) ⭐⭐
These problems blend regex with concepts from earlier chapters.
M.1. (Ch 7 + Ch 22) Solve this problem two ways — once using only string methods, and once using regex. Which is cleaner?
Given a string, count how many words start with a capital letter.
text = "Alice and Bob went to Paris but not to london"
M.2. (Ch 10 + Ch 22) Write a script that reads a text file line by line and prints only lines that contain an IPv4 address (four groups of 1-3 digits separated by dots, like 192.168.1.1). Don't worry about validating the number ranges — just the format.
M.3. (Ch 9 + Ch 22) Given a list of log entries, use regex and a dictionary to count how many times each log level (INFO, WARNING, ERROR) appears:
logs = [
"2025-03-14 08:23:17 ERROR disk full",
"2025-03-14 08:24:01 INFO started",
"2025-03-14 08:25:33 ERROR timeout",
"2025-03-14 08:26:44 INFO recovered",
"2025-03-14 08:27:12 WARNING low memory",
]
M.4. (Ch 6 + Ch 22) Write a function validate_password(password: str) -> tuple[bool, list[str]] that checks a password against these rules and returns whether it's valid plus a list of failure reasons:
- At least 8 characters
- Contains at least one uppercase letter
- Contains at least one lowercase letter
- Contains at least one digit
- Contains at least one special character (
!@#$%^&*)
Use re.search() for each check.
Part E: Research & Extension ⭐⭐⭐⭐
E.1. Research "ReDoS" (Regular Expression Denial of Service). Write a short explanation (200 words) of what it is and why certain regex patterns are vulnerable. Give one example of a "catastrophic backtracking" pattern.
E.2. Python's re module uses a backtracking NFA regex engine. Research the difference between NFA and DFA regex engines. What are the trade-offs? Which does grep use? Which does Python use? Write a 200-word comparison.
E.3. Explore the regex third-party module (pip install regex). What features does it offer that re does not? Name at least three and give a use case for each.
Solutions
Selected solutions in appendices/answers-to-selected.md.