Learn Regex in a Single Post: Complete Tutorial From Anchors and Quantifiers to Lookarounds and Backtracking

Regular expressions are the universal language of text search. Every programming language, every text editor, every grep, every validation library speaks regex. A 20-character pattern can find every email in a 10 GB log file, validate a phone number, or extract every URL from an HTML page. The syntax looks cryptic at first, but it is a small grammar that composes powerfully. This single post teaches the whole subject in five stages, with runnable snippets and five diagrams.

Learning Roadmap

Regex Learning Roadmap

The roadmap moves from matching literal characters (Stage 1), to controlling how many and how they group (Stage 2), to the shorthand vocabulary (Stage 3), to zero-width assertions (Stage 4), to the engine itself and how it fails (Stage 5).


Stage 1 — Literals + Character Classes + Anchors

Literals

The simplest regex matches text literally:

hello        # matches "hello" anywhere in the string
cat          # matches "cat", "catalog", "concatenate" — anywhere it appears

The wildcard .

. matches any single character except a newline (unless the /s flag is on):

a.c          # aac, abc, a1c, a@c — any one char between a and c

Character classes [...]

A bracketed set matches any one character inside it:

[aeiou]              # any one vowel
[a-z]                # any lowercase letter
[A-Za-z0-9]          # any letter or digit
[0-9.]               # any digit or a literal dot (inside [], . is literal)
[^0-9]               # negated: any character that is NOT a digit

Inside [...], most metacharacters lose their special meaning — . is a literal dot, ( is a literal paren. The exceptions are ] (closes the class), \ (escape), ^ (negates only if first), and - (range only if between two chars).

Anchors

Anchors don’t match characters — they match positions (zero-width):

^error          # "error" only at the start of a line/string
error$          # "error" only at the end
^\d+$           # entire line is digits
\bword\b        # "word" as a whole word (not "password" or "sword")
\Bword\B        # "word" only when NOT at word boundaries (inside another word)

\b is a word boundary — the position between a word character (\w: letter, digit, underscore) and a non-word character. It’s how you match whole words without matching substrings.

Stage 1 in action

# find lines that start with "ERROR"
grep -E '^ERROR' app.log

# find whole-word "TODO"
grep -E '\bTODO\b' *.py

Regex Building Blocks


Stage 2 — Quantifiers + Groups

Quantifiers: how many

a*        # zero or more a's
a+        # one or more a's
a?        # zero or one a (optional)
a{3}      # exactly 3 a's
a{2,4}    # between 2 and 4 a's
a{2,}     # 2 or more a's

Greedy vs lazy

Quantifiers are greedy by default — they match as much as possible, then backtrack if the rest of the pattern fails:

".*"          # on 'say "hi" and "bye"'  matches '"hi" and "bye"' (too much!)
".*?"         # lazy: matches '"hi"' first, then '"bye"' separately

Add ? after a quantifier to make it lazy (match as little as possible): *?, +?, ??, {n,m}?.

Pitfall: Greedy .* is the #1 regex bug. To match quoted strings, use ".*?" (lazy) or — better, for performance — a negated class "[^"]*" (matches a quote, then any non-quote chars, then a quote). The negated class cannot over-match and is faster.

Capturing groups () and backreferences

(ab)+              # "ab", "abab", "ababab"
(\w+)\s\1          # \1 = backreference to group 1; matches "the the", "ha ha"

Backreferences let a pattern refer back to text it already captured — useful for detecting repeated words or matching balanced delimiters:

import re
re.search(r'(\w+)\s+\1', "this is a a test").group()   # 'a a'

Named groups

(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})    # Python
(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})       # JS / .NET / PCRE

Named groups make complex patterns readable. In Python:

m = re.search(r'(?P<year>\d{4})-(?P<month>\d{2})', "2026-07-13")
m.group("year")    # '2026'
m.group("month")   # '07'

Non-capturing groups

(?:abc)+            # group for repetition, but no capture saved

Use (?:...) when you need grouping for a quantifier but don’t care to keep the captured text — it’s faster and avoids polluting your group numbers.

Alternation

cat|dog|bird         # cat OR dog OR bird
yes|no               # yes or no
\b(cat|dog)\b        # whole-word cat or dog (group for the alternation)

Pitfall: Alternation tries leftmost-first, not longest-match. a|ab on "ab" matches just "a". Order alternatives from most-specific to least-specific when they can overlap.


Stage 3 — Shorthand Classes + Metacharacters

Shorthand character classes

\d       # digit [0-9]
\w       # word char [A-Za-z0-9_]
\s       # whitespace [ \t\r\n\f]
\D       # non-digit        (negated \d)
\W       # non-word char    (negated \w)
\S       # non-whitespace   (negated \s)

Escaping metacharacters

These characters are special in regex: . ^ $ * + ? { } [ ] \ | ( ). To match them literally, escape with a backslash:

\.        # literal dot (e.g. in an IP or filename)
\*        # literal asterisk
\( \)     # literal parens
\\        # literal backslash
\?        # literal question mark

Pitfall: When building regex from strings in code, use the language’s escape function (re.escape in Python, RegExp.escape in JS) to escape user input — otherwise a . in the input becomes a wildcard.

Common recipes (Stage 3 toolbox)

\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}        # naive IPv4
[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}   # naive email
https?://[\w./%-]+                       # http or https URL
\b\d{3}[-.]?\d{3}[-.]?\d{4}\b             # US-style phone
[一-鿿]+                          # one or more CJK characters

These “naive” patterns are fine for extraction and quick validation. For rigorous email/URL validation, use a dedicated library (or the RFC 5322 email regex, which is famously huge) — regex alone can’t validate that a domain’s TLD actually exists.


Stage 4 — Lookarounds + Flags

Lookarounds are zero-width assertions: they check whether text matches a pattern without consuming it. They “look around” the current position.

(?=pattern)       # lookahead:  asserts pattern follows (positive)
(?!pattern)       # lookahead:  asserts pattern does NOT follow (negative)
(?<=pattern)      # lookbehind: asserts pattern precedes (positive)
(?<!pattern)      # lookbehind: asserts pattern does NOT precede (negative)

Lookahead examples

\d+(?=px)         # digits only if followed by "px" — "px" not consumed
\b\w+(?!ing\b)\b  # words NOT ending in "ing"

Lookbehind examples

(?<=\$)\d+        # digits preceded by "$" — "$" not consumed
(?<![\w.])@       # "@" not preceded by a word char or dot (avoid emails-in-urls)

Pitfall: Many engines (JavaScript until 2018, Go’s RE2) support only fixed-width lookbehind — (?<=foo) is fine, (?<=\w+) is not. Python’s re requires fixed-width lookbehind; the third-party regex module allows variable width.

Flags

Flags modify the whole pattern’s behavior (written after the closing / in JS, or as constants in other languages):

Flag Meaning
i case-insensitive
g global — find all matches, not just the first
m multiline — ^ and $ match line boundaries, not just string boundaries
s dotall — . matches newline too
x extended — ignore whitespace and allow # comments in the pattern
// JS: match all case-insensitive "todo" as whole words
const re = /\btodo\b/gi;
// Python: same
re.findall(r'\btodo\b', text, flags=re.IGNORECASE)

/x (verbose) lets you document a complex pattern:

date = re.compile(r"""
    \b
    (?P<year>\d{4}) - (?P<month>\d{2}) - (?P<day>\d{2})   # YYYY-MM-DD
    \b
""", re.VERBOSE)

Atomic groups

(?>pattern)       # atomic group: once it matches, it won't backtrack

Atomic groups (and possessive quantifiers *+, ++, ?+ in PCRE) prevent backtracking into the group — a performance tool and a correctness tool for avoiding runaway matches.


Stage 5 — Engines + Pitfalls

How a regex engine works

Most real-world regex engines are NFA-based (nondeterministic finite automaton): they compile the pattern into a graph of states, then walk the input, trying paths and backtracking when one fails.

Regex Engine

A DFA (deterministic finite automaton) engine (like lex) precomputes all paths, giving guaranteed linear time but no backreferences or lookarounds. Google’s RE2 engine (used in Go’s regexp, Rust’s regex, and rg --pcre2 is a separate path) takes a hybrid approach: it runs in linear time by avoiding backtracking, at the cost of dropping backreferences and some lookarounds.

Catastrophic backtracking

Nested quantifiers can blow up exponentially — the classic ReDoS (regular expression denial of service):

(a+)+b         # on input "aaaaaaaaaaaaaaaaaaaaaaaa!"  -> 2^n paths before failing

The engine tries every partition of the as between the two + quantifiers before giving up. A 30-character input takes ~1 billion steps.

Defenses:

  • Use RE2 (re2 in Python, Go’s regexp) for patterns on untrusted input — linear time, no backtracking.
  • Use atomic groups (?>...) or possessive quantifiers to lock in matches.
  • Avoid nested overlapping quantifiers; rewrite a+a+ as aa+, (a|a)+ as a+.
  • Always test a regex against near-miss inputs (long strings of valid chars with no final match).

Pitfall: A regex that “works on my test cases” can hang in production if an attacker sends a crafted string. Treat regex on untrusted input like any other parser: bound it (RE2) or fuzz it.

Flavors (engines differ)

Flavor Where Notes
PCRE PHP, Apache, grep -P richest features (backrefs, lookaround, atomic)
JavaScript browsers, Node no lookbehind until ES2018; no atomic groups
Python re CPython stdlib fixed-width lookbehind; use regex module for variable width
Go regexp Go RE2 — no backrefs, linear time
Java java.util.regex backrefs, lookaround
Rust regex Rust RE2-like, linear time, no backrefs
POSIX BRE/ERE grep, sed limited; ERE = grep -E

Regex Flavors and Tools

Pitfall: A pattern that works in your test tool (regex101, often PCRE) may not work in your target language. Always test in the actual engine you’ll run. Use the “flavor” selector on regex101 to match.

CLI tools that speak regex

grep -E 'pattern' file         # ERE (extended)
grep -P 'pattern' file         # PCRE (GNU grep)
sed -E 's/pat/repl/g' file     # extended regex substitution
awk '/pattern/' file           # awk uses ERE
rg 'pattern'                    # ripgrep — fast, ERE by default
rg 'pat' --pcre2               # use PCRE2 (lookarounds, backrefs)

Testing + dev tools

  • regex101.com — interactive tester; pick your engine flavor, see an explanation of each token, share patterns.
  • regexr.com — similar, with a cheat sheet sidebar.
  • debuggex.com — renders the regex as a visual railroad diagram.
  • grexgenerates a regex from a list of sample strings (great for building extraction patterns).

Quick-Start Checklist

  1. Learn the 6 metacharacters that matter most: . * + ? [ ] \. The rest build on these.
  2. Master anchors ^ $ \b — they turn “matches anywhere” into “matches exactly here”.
  3. Use character classes [a-z0-9_] instead of chains of | alternatives.
  4. Beware greedy .* — prefer [^"]* or lazy .*? for delimited matches.
  5. Name your groups (?P<year>\d{4}) — readable and self-documenting.
  6. Add the /x flag to document any pattern longer than one line.
  7. Test on regex101 with the right engine flavor before deploying.
  8. Bound untrusted input — use RE2 (re2/Go/Rust) or atomic groups to avoid ReDoS.
  9. Escape user input with re.escape / RegExp.escape when building patterns from strings.
  10. Write recipes once (email, phone, URL, date, IP) and reuse them — don’t reinvent.

Common Pitfalls

  • Greedy .* over-matching — use "[^"]*" or ".*?" for quoted strings.
  • Forgetting \b\bword\b matches whole words; word matches substrings too.
  • . doesn’t match newline — use the /s (dotall) flag if you need it to.
  • Leftmost alternationa|ab matches a first; order by specificity.
  • Unescaped metacharacters in input — use re.escape.
  • Catastrophic backtracking from nested quantifiers like (a+)+ — use RE2 or atomic groups.
  • Fixed-width lookbehind only in JS/Python re — use the regex module for variable width.
  • Wrong flavor — testing in PCRE then running in Go (RE2) breaks on backreferences.

Further Reading

Regex is the universal text-processing tool — these adjacent PyShine tutorials use it everywhere:


Regex is a small grammar with a long tail. The five stages here — literals and classes, quantifiers and groups, shorthand and escapes, lookarounds and flags, engines and backtracking — cover ~95% of what you will ever write, and the remaining 5% is well-documented on regex101 and RexEgg. The skill that separates a regex amateur from a professional is not knowing more syntax; it is knowing the pitfalls — greedy matching, leftmost alternation, catastrophic backtracking, and the engine-flavor differences. Write a pattern a day, test it on regex101 with near-miss inputs, and within two weeks you’ll read regex like prose.

Watch PyShine on YouTube

Contents