102. Re Module — Regular Expressions
Pattern matching and text extraction with regex
102. Re Module — Regular Expressions
Rohan saves reports as report_20260307_143022.json. Now he needs to extract the date and time from the filename, validate it, and find all reports within a date range.
He tries string slicing first, then realizes his filenames might come from different sources with slight variations. He reaches for regex:
import re
def parse_report_filename(filename):
# Anchors (^ and $) ensure the whole string matches — not just a substring
pattern = r"^report_(\d{4})(\d{2})(\d{2})_(\d{6})\.json$"
match = re.match(pattern, filename)
if not match:
return None
year, month, day, time = match.groups() # .groups() → tuple of all captures
return {"year": year, "month": month, "day": day, "time": time}Without the ^ and $ anchors, "bad_prefix_report_20260307_143022.json" would match — the pattern would find the valid part inside the longer string.
He also scans log lines for RAM warnings:
LOG_PATTERN = r"(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}) (WARNING|ERROR|CRITICAL): (.+)"
def extract_log_entry(line):
match = re.match(LOG_PATTERN, line)
if match:
timestamp, level, message = match.groups()
return {"timestamp": timestamp, "level": level, "message": message}
return NoneAlways use raw strings for patterns:
r"\d+"passes\ddirectly to the regex engine. Withoutr, Python interprets\dfirst and sends something unexpected. Every regex pattern should start withr".
💡 Fun fact: Regular expressions were invented by mathematician Stephen Kleene in the 1950s to describe patterns in formal language theory. The re module in Python is based on Perl’s regex syntax. The r"" raw string prefix is unique to Python — most other languages use backslash-escaped strings or built-in regex literals like JavaScript’s /pattern/.
⚠️ Watch out: re.match() only matches at the beginning of the string — it doesn’t require the pattern to match the ENTIRE string. "hello world" would match re.match(r"hello", "hello world"). To ensure a full-string match with re.match(), always add $ at the end of your pattern. Alternatively use re.fullmatch() which requires the pattern to cover the entire string.
🤔 Think about it: re.search() finds the first match anywhere in a string, while re.findall() returns all matches. If you use re.findall() with capture groups (), what does it return — a list of strings or a list of tuples? Try it with re.findall(r"(\w+)@(\w+)", "a@b c@d").
Learning objectives
- Use re.match() for full-string matching and re.search() for substring matching
- Always prefix regex patterns with r““ (raw strings)
- Use ^ and $ anchors to ensure full-string matching
- Extract capture groups with .groups() for tuple unpacking
- Use re.findall() and len() to count pattern occurrences
Key concepts
- re.match() vs re.search() — anchored vs anywhere
- Raw string prefix r““ — prevents escape confusion
- ^ and $ anchors — start/end of string
- Capture groups () and .groups()
- re.findall() — returns list of all matches
Try it
Concept detail
Regular Expressions with the re Module
Key Functions
| Function | What it does |
|---|---|
re.match(pattern, string) | Match at the START of string |
re.search(pattern, string) | Match ANYWHERE in string |
re.findall(pattern, string) | Return list of ALL matches |
re.sub(pattern, repl, string) | Replace matches |
re.split(pattern, string) | Split by pattern |
Always Use Raw Strings
# Wrong: Python interprets \d before regex sees it
pattern = "\d+"
# Right: backslashes passed directly to regex engine
pattern = r"\d+"Anchors
^— start of string (or start of line with re.MULTILINE)$— end of string Without anchors,re.search(r"hello", "say hello world")still matches. Usere.match(r"^hello$", s)to require the entire string to match.
Groups
match = re.match(r"(\w+) (\w+)", "John Doe")
match.group(0) # "John Doe" (whole match)
match.group(1) # "John"
match.group(2) # "Doe"
match.groups() # ("John", "Doe") — use for unpackingCommon Patterns
r"\d+" # one or more digits
r"\w+" # word characters (letters, digits, _)
r"[a-z]+" # lowercase letters only
r".+" # any character (except newline), one or more
r"https?://" # http:// or https://Solution
import re
def is_valid_email(email: str) -> bool:
"""Return True if email matches a basic email pattern."""
# ^ and $ anchors ensure the ENTIRE string must match
pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
return bool(re.match(pattern, email))
def extract_log_parts(log_line: str):
"""Extract (date, level, message) from a log line.
Format: '2025-03-07 ERROR: Connection failed'
Returns tuple or None if no match.
"""
pattern = r"(\d{4}-\d{2}-\d{2}) (\w+): (.+)"
match = re.match(pattern, log_line)
if match:
return match.groups() # returns ('2025-03-07', 'ERROR', 'Connection failed')
return None
def count_words(text: str) -> int:
"""Count words in text (sequences of word characters)."""
return len(re.findall(r"\w+", text))Tests
def test_valid_email_simple():
assert is_valid_email("[email protected]") is True
def test_valid_email_with_dots():
assert is_valid_email("[email protected]") is True
def test_invalid_email_no_at():
assert is_valid_email("notanemail.com") is False
def test_invalid_email_no_domain():
assert is_valid_email("user@") is False
def test_invalid_email_partial_match():
# This is the key test — anchors prevent partial matches
assert is_valid_email("bad@@hello@[email protected]") is False
def test_extract_log_parts_returns_tuple():
result = extract_log_parts("2025-03-07 ERROR: Connection failed")
assert isinstance(result, tuple), "Should return a tuple of groups"
def test_extract_log_parts_correct_values():
date, level, message = extract_log_parts("2025-03-07 ERROR: Connection failed")
assert date == "2025-03-07"
assert level == "ERROR"
assert message == "Connection failed"
def test_extract_log_parts_no_match():
result = extract_log_parts("not a log line at all")
assert result is None
def test_count_words_basic():
assert count_words("hello world") == 2
def test_count_words_returns_int():
result = count_words("one two three")
assert isinstance(result, int), "count_words must return an int"
def test_count_words_empty():
assert count_words("") == 0