Skip to content

Python Regex

Regular expressions for finding, extracting, and replacing text with Python's built-in re module. Essential for cleaning messy data, parsing logs, and validating input.

Table of Contents

Installation / Setup

re is built into Python — no install needed.

import re

Always write patterns as raw strings (r"...") so backslashes are literal.

Core Syntax

Main functions

Function Returns Use
re.search(pattern, s) First match or None Find anywhere in string
re.match(pattern, s) Match at start or None Anchored at beginning
re.fullmatch(pattern, s) Match if entire string matches Validation
re.findall(pattern, s) List of all matches Extract
re.finditer(pattern, s) Iterator of match objects Large text
re.sub(pattern, repl, s) New string Replace
re.split(pattern, s) List Split
re.compile(pattern) Compiled pattern Reuse
m = re.search(r"\d+", "order #42 total 99")
m.group()        # '42'
m.start()        # 7 (index of match)

Pattern elements

Element Matches
. Any char except newline
\d \w \s Digit, word char ([A-Za-z0-9_]), whitespace
\D \W \S Negations of the above
[abc] [a-z] Any of / range of chars
[^abc] Not any of
^ $ Start / end of string
\b Word boundary
a* a+ a? Zero+, one+, zero-or-one
a{2,4} 2 to 4 repetitions
(abc) (?P<name>abc) Group / named group
a\|b Alternation (a or b)
\d+? Lazy (minimal) match
(?=x) (?!x) Lookahead: followed by / not followed by
(?<=x) (?<!x) Lookbehind: preceded by / not preceded by
\1 Backreference to group 1
re.escape(s) Literal-match a string

Flags

Flag Effect
re.IGNORECASE (re.I) Case-insensitive
re.MULTILINE (re.M) ^/$ match per line
re.DOTALL (re.S) . matches newlines
re.VERBOSE (re.X) Whitespace/comments in pattern
re.ASCII (re.A) \w/\d ASCII-only
re.search(r"error", s, re.IGNORECASE)
re.search(r"""
    (?P<ip>\d+\.\d+\.\d+\.\d+)   # IP address
    \s+
    (?P<status>\d{3})            # HTTP status
""", line, re.VERBOSE)

Common Operations

Extract numbers

re.findall(r"-?\d+\.?\d*", "temps: -3.5, 0, 12.25")   # ['-3.5', '0', '12.25']

Find email-like patterns

re.findall(r"[\w.+-]+@[\w-]+\.[\w.]+", text)

Named groups

m = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})", "date: 2024-03")
m.group("year")     # '2024'
m.groupdict()       # {'year': '2024', 'month': '03'}

Replace with callbacks

re.sub(r"\d{4}", lambda m: f"[{m.group()}]", "2024")   # '[2024]'

Split on multiple delimiters

re.split(r"[,;\s]+", "a, b;c d")   # ['a', 'b', 'c', 'd']

Practical Examples

1. Clean a messy price column

import re

raw = ["$1,299.00", "1 299", "USD 1,299.50", "n/a"]
def clean_price(s):
    digits = re.sub(r"[^\d.]", "", s)     # keep digits and dots
    return float(digits) if digits else None

[clean_price(x) for x in raw]    # [1299.0, 1299.0, 1299.5, None]

2. Parse log lines

pattern = re.compile(
    r"(?P<ts>\d{4}-\d{2}-\d{2} \d{2}:\d{2}) "
    r"(?P<level>INFO|ERROR|WARN) (?P<msg>.*)"
)
for line in open("app.log", encoding="utf-8"):
    m = pattern.search(line)
    if m and m.group("level") == "ERROR":
        print(m.group("msg"))

3. Extract key-value pairs

re.findall(r"(\w+)=(\S+)", "lr=0.01 epochs=50 batch=32")   # [('lr', '0.01'), ...]

Common Patterns

# Strip HTML tags (simple cases)
re.sub(r"<[^>]+>", "", html)

# Normalize whitespace
re.sub(r"\s+", " ", text).strip()

# Slugify a title
slug = re.sub(r"[^a-z0-9]+", "-", title.lower()).strip("-")

# Detect if a string is a valid simple email
bool(re.fullmatch(r"[\w.+-]+@[\w-]+\.[\w.]+", email))

# Lazy vs greedy: first vs last match
re.search(r"<.*>",  "<a><b>").group()    # '<a><b>' (greedy)
re.search(r"<.*?>", "<a><b>").group()    # '<a>'   (lazy)

Best Practices

  • Always use raw strings (r"...") for patterns.
  • re.compile patterns used in loops for speed and clarity.
  • Use re.VERBOSE + named groups for anything non-trivial.
  • Prefer re.fullmatch for validation, re.search for finding.
  • Test patterns on edge cases: empty string, Unicode, \n, very long input.
  • Use re.escape() when matching user input literally.
  • Watch for catastrophic backtracking with nested quantifiers like (a+)+.

Common Mistakes

Mistake Consequence Correct approach
"\\d+" instead of r"\d+" Matches d literally or \ escapes Use raw strings
re.match when searching anywhere Match fails mid-string Use re.search
Greedy quantifiers in HTML/JSON Over-matches across tags Use lazy .*?
Unescaped . meaning "any char" Matches more than intended Escape: \.
Recompiling in a loop Slow re.compile once
Complex pattern with nested */+ Hangs on adversarial input Simplify; avoid (a+)+

Troubleshooting

Symptom Cause Fix
re.error: missing ), unterminated subpattern Unbalanced parentheses Count groups; check (?:...) non-capturing
Pattern matches nothing Wrong anchor / flag Test on regex101; try re.search vs match
'NoneType' object has no attribute 'group' No match found Guard: if m: m.group()
Backtracking timeout / freeze Nested quantifiers Simplify pattern; use possessive/atomic alternatives
\d matches Arabic-Indic digits Unicode by default Add re.ASCII

Official Documentation

Last Updated

2026-09-06 — Initial version.