// fundamentals · text

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 Regex — one pattern language, every tool a pattern is a tiny machine engine tries each start position, left to right first match wins, then it stops from Kleene (1951) via Thompson (1968) to your editor the core notation literals match themselves; . matches any one char classes: [abc], [^abc], \d digits, \w word chars quantifiers: * 0+, + 1+, ? 0/1, {n} exactly n greedy + backtracking quantifiers grab the MOST, then give back ".*" swallows both quoted strings — classic trap fix boundaries with classes: "[^"]*" anchors ^ start, $ end — together: whole-string match unanchored patterns match ANYWHERE inside routes/validators always anchor both ends groups (…) captures what matched inside (?P<name>…) names it — matching becomes extraction a failed group fails the whole match = free validation four dialects, one language Python re / JavaScript / grep -E / PostgreSQL ~ core notation identical, group syntax drifts learn once, use everywhere when not to regex HTML/JSON need real parsers, not patterns if the pattern needs a comment, split it str methods beat regex for fixed text

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:

patternmatches
chaiexactly 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], \done digit (two spellings of the same thing)
\wone "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|dogthe 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 email are 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