Python Regex cheat sheet
Matching, capture groups, lookaround, flags, and substitution patterns for parsing logs, filenames, and messy source columns.
Core functions
pattern = re.compile(r"^(\d{4})-(\d{2})-(\d{2})")- Compile once at module level when the pattern runs per row. The internal cache holds only 512 entries and is keyed on the exact pattern string.
m = pattern.search(text)- search scans the whole string, match anchors at position zero, and fullmatch requires the entire string. Most bugs are match where search was intended.
value = m.group(1) if m else None- A failed match returns None rather than an empty match, so any attribute access on the result raises AttributeError. Guard every single call.
re.findall(r"(\w+)=(\S+)", line)- With one group findall returns strings, with several it returns tuples, and with none it returns whole matches. That shape change breaks callers silently.
for m in re.finditer(pattern, payload): ...- finditer yields match objects lazily and exposes span offsets. Prefer it over findall on multi-megabyte log payloads to keep memory flat.
Character classes and quantifiers
r"\d+\.\d{2}"- The dot is literal inside a character class and needs escaping outside one. \d also matches non-ASCII digits unless you add the re.ASCII flag.
r"[^,\n]*"- A negated class is the workhorse of field extraction because it physically cannot run past the delimiter, unlike .* which happily does.
r"<(.+?)>"- The trailing question mark makes the quantifier lazy so it stops at the first closing bracket. Without it one match swallows the entire line.
r"\s*\|\s*"- Splitting on a padded pipe. The pipe means alternation and must be escaped, otherwise the pattern reads as "empty or empty" and matches everywhere.
r"(?:ab){2,4}"- Bounded repetition of a group. Prefer explicit bounds over plus when parsing untrusted input, since they cap how far the engine can backtrack.
Groups and captures
r"(?P<year>\d{4})-(?P<month>\d{2})"- Named groups read far better than positional indexes, and m.groupdict() returns a dict that drops straight into a row constructor.
m.group(0), m.start(), m.end()- Group zero is the full match. start and end give character offsets into the original string, which is how you slice out surrounding context.
r"(?:https?)://(\S+)"- The (?:...) form groups without capturing, keeping findall output flat and sparing you from renumbering every group when the pattern changes.
r"\b(\w+)\s+\1\b"- A backreference matching a repeated word. Backreferences also disable most engine optimizations, so keep them out of hot per-row paths.
m.groups(default="")- Unmatched optional groups come back as None, which then breaks a join or a typed Parquet write. Supply the default at extraction time instead.
Anchors and lookaround
r"^ERROR\b"- Caret means start of string unless re.MULTILINE is set, when it becomes start of line. The word boundary is what stops it matching ERRORS.
r"\Aid_\d+\Z"- \A and \Z always mean start and end of the whole string regardless of MULTILINE. Prefer them over caret and dollar when validating a full value.
r"(?<=user_)\w+"- Lookbehind asserts the prefix without consuming it, so it stays out of the result. Python requires lookbehind to be fixed width.
r"amount(?=\s*=)"- Lookahead checks what follows without capturing it, which is how you split on a delimiter that has to stay attached to the next field.
r"^(?!#)(.+)$"- Negative lookahead to skip comment lines. Filtering inside the engine is cheaper than matching everything and discarding rows in Python.
Flags
re.compile(pattern, re.IGNORECASE | re.MULTILINE)- Flags combine with the bitwise or. Passing them positionally to re.sub is a classic error, because that argument slot is the replacement count.
re.DOTALL- Makes the dot match newlines. Required when a value spans lines, and its absence is why a greedy .* quietly stops at the first line break.
re.VERBOSE- Lets you spread a pattern across lines with comments. Literal whitespace becomes insignificant, so real spaces must be escaped or put in a class.
re.ASCII- Restricts \w, \d, and \b to ASCII. Without it \w matches accented letters and \d matches Devanagari digits, which is rarely what a validator wants.
r"(?i)^error"- A global inline flag must appear at the very start of the pattern since Python 3.11 or it raises. The scoped form (?i:...) applies to one group.
Substitution and splitting
re.sub(r"\s+", " ", value).strip()- The standard whitespace normalizer before loading a text column. One pass beats a chain of replace calls for tabs, newlines, and doubled spaces.
re.sub(r"(\d{4})-(\d{2})", r"\2/\1", s)- Backreferences in the replacement use the \1 form. Always make the replacement a raw string or Python consumes the backslash before re sees it.
re.sub(r"\b\d{13,19}\b", lambda m: "*" * len(m.group()), text)- A callable replacement allows per-match logic, here masking card-shaped numbers before any log line leaves the process.
re.subn(pattern, repl, text)- Returns the replacement count alongside the result, which turns a silent cleanup step into an auditable metric you can alert on.
re.split(r"[;,|]\s*", line)- Splits on any of several delimiters. For real CSV use the csv module instead, because a regex cannot track whether it is inside a quoted field.
Performance and pitfalls
r"^(a+)+$"- Nested quantifiers cause catastrophic backtracking, where a 30-character non-matching input can run for minutes. Rewrite as a single quantifier.
re.escape(user_value)- Mandatory whenever the pattern is built from data. One unescaped dot or bracket from a filename turns an exact lookup into a wildcard match.
if "ERROR" in line and pattern.search(line): ...- A plain substring test short-circuits before the engine runs. On log scanning this routinely cuts total runtime by an order of magnitude.
line.startswith("2026-") and line.split("|", 3)- String methods are several times faster than regex for fixed text. Reach for re only when the shape genuinely varies between rows.
import regex # atomic groups, variable-width lookbehind- The third-party regex module adds possessive quantifiers, atomic groups, and variable-width lookbehind when the stdlib pattern turns ugly.
From DataLane — tutorials at/blog, practice SQL live in theplayground.