← Home

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 None

Always use raw strings for patterns: r"\d+" passes \d directly to the regex engine. Without r, Python interprets \d first and sends something unexpected. Every regex pattern should start with r".

💡 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

FunctionWhat 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. Use re.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 unpacking

Common 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

Resources