DataLane
← All cheat sheets

Python Regex cheat sheet

Matching, capture groups, lookaround, flags, and substitution patterns for parsing logs, filenames, and messy source columns.

ProgrammingIntermediate7 sections

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.

↑↓ navigate openesc close