Regex looks like a cat walked across the keyboard, but it's five concepts wearing dense syntax. Learn the five, keep the pattern library below bookmarked, and know the two traps (greedy matching, unescaped dots) that cause 90% of regex bugs. The audio explains the thinking — the symbols are here on the page when you need them.
A regex describes the shape of text: "three digits, a dash, four digits". The engine slides that description along your string looking for a place it fits. That's the entire mechanism. And the first rule of regex: if you're looking for fixed text, you don't need regex — in / includes() is faster and can't surprise you.
# shape: "digits, dash, digits" — matches phone extensions
\d{3}-\d{4}
"call 555-0199 today" → matches "555-0199"
# but fixed text needs NO regex:
✗ re.search("error", line) # overkill
✓ "error" in line # same result, no surprises
A class answers "what characters may stand in this position?" Three shorthands cover most needs — digit, word-character, whitespace — plus square brackets for a custom allowlist, and a caret inside them for a blocklist.
\d # one digit 0-9
\w # one "word" char letters, digits, _
\s # one whitespace space, tab, newline
. # ANY one character (except newline) — careful, see 07
[abc] # exactly one of: a, b, or c
[a-f0-9] # one hex digit (ranges with -)
[^0-9] # one char that is NOT a digit (^ inside [] = not)
\D \W \S # capitals = the opposite of their lowercase twin
A quantifier sits after a thing and says how many times it repeats. Four cover everything: optional, any number, at least one, and exact counts in braces.
colou?r # ? = 0 or 1 → color, colour
go+al # + = 1 or more → goal, gooooal
ab*c # * = 0 or more → ac, abc, abbbc
\d{4} # exactly 4 → 2026
\d{2,4} # 2 to 4 → 26, 202, 2026
\d{2,} # 2 or more
# reading practice — a simple date shape:
\d{4}-\d{2}-\d{2} # 2026-07-22
The most important 90 seconds on this page. Quantifiers are greedy: they grab as much as possible while still letting the match succeed. Combine that with dot-star and you get regex's most classic bug — a match that spans from the first opening to the last closing, swallowing everything between.
text: <b>bold</b> and <i>italic</i>
<.*> # greedy: matches <b>bold</b> and <i>italic</i> — ALL of it!
<.*?> # lazy (add ?): matches <b>, then </b>, then <i>... ✓
<[^>]*> # often better: "anything except the closer" — fast & explicit
? after the quantifier) or — usually better — replace the dot with a negated class: "anything except the ending character". The negated class says what you actually mean.
Anchors match positions, not characters: start of string, end of string, edge of a word. They fix the sneaky class of bug where your pattern is right but matches in the wrong place — validating "abc123abc" as a number because \d+ found the 123 in the middle.
^\d+$ # ^ start, $ end → the WHOLE string is digits
# without them: "abc123abc" passes! (finds 123 inside)
\bcat\b # \b = word boundary → "cat" the word,
# not the cat in "category" or "concatenate"
^ERROR # lines STARTING with ERROR (with multiline flag)
Parentheses do two jobs: they bundle (so a quantifier or alternation applies to the whole chunk) and they capture (the matched part comes back to your code, numbered left to right). Alternation — the pipe — means "or".
# bundle + or:
\.(jpg|png|gif)$ # files ending in any of the three
# capture: pull the parts OUT of a date
(\d{4})-(\d{2})-(\d{2})
# └ group 1: year └ 2: month └ 3: day
# in Python:
m = re.search(r"(\d{4})-(\d{2})-(\d{2})", text)
m.group(1) # "2026"
# swap with backreferences: "Lastname, First" → "First Lastname"
re.sub(r"(\w+), (\w+)", r"\2 \1", "Lovelace, Ada")
Fifteen characters have special meanings. Use one literally and forget the backslash, and your pattern silently matches more than you meant — the classic being the unescaped dot in domains and prices, which means "any character at all".
# special characters (escape with \ to mean them literally):
. * + ? ( ) [ ] { } ^ $ | \ /
swiftgrasp.com # the . matches ANY char:
# also matches "swiftgraspXcom" ⚠
swiftgrasp\.com # ✓ a literal dot
$19.99 # $ means "end of string" — never matches!
\$19\.99 # ✓ literal dollar, literal dot
swiftgrasp.com does match "swiftgrasp.com". Your tests pass. It just also matches things it shouldn't, and you find out in production, from a weird edge case. Silent over-matching is why regexes need negative tests: assert what must not match.
The patterns everyone actually needs, tested and annotated. One honest note first: the "perfect email regex" is a famous trap — the spec allows monstrosities, so production systems use a simple pattern plus a confirmation email. Pragmatism beats spec-lawyering.
# email (pragmatic — pair with a confirmation mail)
^[\w.+-]+@[\w-]+\.[\w.]+$
# URL (http/https)
https?://[^\s]+
# ISO date 2026-07-22
\b\d{4}-\d{2}-\d{2}\b
# time 14:30 or 9:05
\b\d{1,2}:\d{2}\b
# number with optional decimals (prices, amounts)
-?\d+(\.\d+)?
# IPv4 (pragmatic)
\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\b
# collapse repeated whitespace → single space
\s+ # replace with " "
# extract everything between quotes
"([^"]*)"
# hex color #fff or #1a2b3c
#[0-9a-fA-F]{3,6}\b
The most senior regex skill is declining to use it. Three honest rules: fixed text needs no regex; nested structures (HTML, JSON, code) can't be parsed with regex — that's a mathematical limit, not a skill issue — use a real parser; and if the pattern doesn't fit on one line, future-you will curse present-you. Regex is a scalpel, not a chainsaw.
✗ parsing HTML with regex # use an HTML parser (BeautifulSoup...)
✗ parsing JSON with regex # use json.loads — it exists, it's right there
✗ a 200-char regex # split into steps, or write a small parser
✓ log line extraction # regex's home turf
✓ validation (anchored!) # with negative tests
✓ find & replace in editor # daily superpower
Everything above, compressed.
# classes # quantifiers
\d digit [abc] one of ? 0-1 * 0+
\w word [a-z] range + 1+ {n,m} counted
\s space [^x] not x add ? after → lazy version
# anchors # groups
^ start $ end (x) capture
\b word edge (a|b) or
validate = ^...$ always \1 backreference
# the two traps
.* too greedy → .*? or [^X]*
literal . $ + ? → escape: \. \$ \+ \?
# debug
regex101.com — explain, test, save
# decline politely
fixed text → in/includes · HTML/JSON → real parser
That completes the beginner developer stack: Python → Git → command line → SQL → HTTP → Docker → regex. Seven pages, about seventy minutes of audio, and a working vocabulary for your first year of building things. Go build one.