How regular expressions work: one pattern language for every tool
A regex is a tiny machine that reads text one character at a time — and the same notation works in your editor, grep, Python, JavaScript, and PostgreSQL. Three simulators show the engine walking a string, greedy quantifiers backtracking into a classic trap, and capture groups turning matching into extraction.
mindmap — quick refresh
Prerequisites: none — this guide stands on its own. (If you came from How WSGI works, this is the full story behind its four-symbol primer.)
Regular expressions look like line noise and behave like magic, so start with three facts that remove the magic. One: the idea is older than programming languages — Stephen Kleene described "regular events" in 1951, and Ken Thompson put them in a text editor in 1968; grep (1973) is literally "global regular expression print". Two: a regex is not smart — it compiles into a tiny machine that reads text one character at a time and answers yes/no. Three: because the notation predates every modern language, the same pattern works in your editor's find box, grep, Python, JavaScript, and PostgreSQL. Learn it once; use it everywhere, forever.
A pattern is a tiny machine
The simplest regex is just literal characters: the pattern chai means the character c, then h, then a, then i, adjacent, in that order. What the engine does with it is mechanical: try to match at position 0 of the string; if that fails, slide to position 1 and try again; repeat until a match succeeds or the string runs out. Watch it hunt through masala chai, 60:
Two behaviors from that walk explain most regex results you'll ever see: the engine reports the first match (leftmost, not "best"), and a failed search costs one cheap attempt per position — which is why searching a huge log for a fixed word is fast.
The core notation
Everything else is a small vocabulary layered on that machine. Each symbol still consumes characters one at a time — it just changes which characters are acceptable:
| pattern | matches |
|---|---|
chai | exactly those characters, in order |
. | any single character (except newline) |
[abc] | one character: a or b or c |
[^abc] | one character that is not a, b, or c |
[0-9], \d | one digit (two spellings of the same thing) |
\w | one "word" character: letter, digit, or _ |
x* | zero or more xs |
x+ | one or more xs |
x? | zero or one x (makes x optional) |
x{3} | exactly three xs |
cat|dog | the text cat, or the text dog |
Read patterns aloud and they stop being noise: \d+ is "one or more digits"; colou?r is "colo, optional u, r" (matches both spellings); [A-Z]\w* is "a capital letter, then any word characters". That's the whole reading skill — left to right, one piece at a time.
Greedy quantifiers and backtracking
Quantifiers hide the engine's one genuinely surprising behavior. * and + are greedy: they first grab as much text as possible, and if the rest of the pattern then can't match, the engine backtracks — gives characters back one at a time until it can. Usually you never notice. But watch the classic trap: trying to match one quoted string with ".*":
The greedy .* ran to the end of the line and backtracked only to the last quote — matching two quoted strings and everything between them. The fix is to give the quantifier a boundary it cannot cross: "[^"]*" ("a quote, then any characters that aren't quotes, then a quote") can never swallow the closing quote. This one lesson — quantifiers maximize, classes constrain — debugs the majority of "my regex matched too much" surprises.
Anchors: whole-match thinking
By default a pattern matches anywhere inside the string — \d+ happily finds 42 in order-42-final. Two anchors pin it down: ^ demands the match start at the beginning, $ demands it end at the end. Together they flip the question from "does this appear somewhere?" to "is the entire string exactly this shape?" — which is what routing and validation need. That's why the route table in How WSGI works wraps every pattern in ^…$: ^/students/\d+/$ must reject /students/42/delete, not find a match inside it.
Groups: from matching to extracting
Parentheses do double duty. They group (so a quantifier can apply to several characters: (ha)+ matches hahaha), and they capture — the engine remembers what the parenthesized part matched, so a yes/no test becomes data extraction. Name the groups and the pattern documents itself:
(?P<name>\w+) - (?P<price>\d+)
Against momo - 150, this yields name = "momo" and price = "150". And because a group can only capture if the whole match succeeds, malformed lines are rejected for free — extraction and validation are the same act:
You've met this before: the WSGI route ^/students/(?P<sid>\d+)/$ captures sid and hands it to your view as a keyword argument. Django's path("students/<int:sid>/", ...) is the same capture wearing friendlier syntax.
The same pattern, four dialects
The claim was "learn once, use everywhere" — here's the proof. One job: find valid menu lines shaped like momo - 150. Same pattern, four tools:
# Python — the re module
import re
m = re.match(r"^(?P<name>\w+) - (?P<price>\d+)$", line)
if m:
print(m["name"], m["price"])// JavaScript — regex literals built into the language
const m = line.match(/^(?<name>\w+) - (?<price>\d+)$/);
if (m) console.log(m.groups.name, m.groups.price);# grep -E on the command line — filter a whole file per line
grep -E '^[[:alnum:]_]+ - [0-9]+$' menu.txt-- PostgreSQL — the ~ operator matches a regex
SELECT * FROM menu_lines WHERE line ~ '^\w+ - \d+$';
The core notation — anchors, classes, quantifiers — is identical everywhere. What drifts at the edges: named-group syntax ((?P<n>…) in Python, (?<n>…) in JavaScript), and classic grep predates \d, so POSIX spells it [0-9]. When a pattern misbehaves in a new tool, suspect the dialect edges, never the core.
When not to reach for regex
Regex is a scalpel, not a chainsaw. Three boundaries worth respecting:
- Nested structure is out of its league. HTML, JSON, and code have arbitrary nesting; regular expressions mathematically cannot count nesting depth. Use a real parser (
html.parser,json.loads) — a regex that "mostly works" on HTML is a bug with a delay. - Fixed text doesn't need a pattern.
line.startswith("ERROR")and"@" in emailare clearer and faster than the equivalent regex. Reach for regex when the text varies in shape, not just to look terse. - If it needs a comment, split it. A 200-character pattern is write-only code. Break it into named pieces, or use several small matches — future-you is the reader who matters.
Takeaways
- A regex is a dumb, fast machine — it tries each start position left to right, one character at a time, and stops at the first match. No magic to memorize, just mechanics.
- Read patterns aloud, left to right —
\d+is "one or more digits". The core vocabulary is ~a dozen symbols; everything else is composition. - Greedy quantifiers maximize, then backtrack — when a match is "too big" (
".*"swallowing two strings), constrain the quantifier with a class:"[^"]*". - Anchor validators and routes with
^…$— otherwise you're asking "does it appear somewhere?" when you mean "is it exactly this shape?" - Groups turn matching into extraction —
(?P<name>…)captures data and validates it in the same pass; it's how WSGI and Django routes hand values to your views. - Know the tool's limits — real parsers for nested formats, string methods for fixed text, and small named patterns over 200-character monsters.
References
- Friedl, J. E. F. (2006). Mastering regular expressions (3rd ed.). O'Reilly Media.
- MDN Web Docs. (n.d.). Regular expressions (JavaScript guide). Mozilla. Retrieved August 17, 2026, from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_expressions
- PostgreSQL Global Development Group. (n.d.). Pattern matching (PostgreSQL documentation). Retrieved August 17, 2026, from https://www.postgresql.org/docs/current/functions-matching.html
- Python Software Foundation. (n.d.). re — Regular expression operations (Python documentation). Retrieved August 17, 2026, from https://docs.python.org/3/library/re.html
- Thompson, K. (1968). Programming techniques: Regular expression search algorithm. Communications of the ACM, 11(6), 419–422. https://doi.org/10.1145/363347.363387