Most regex tutorials teach you to match "cat" or "dog" in a sentence and then wonder why you still can't write a pattern to validate a phone number six months later. The problem isn't that regex is hard — it's that toy examples don't build the muscle you actually need. So let's go through the building blocks using patterns you'll genuinely use.
Literal characters — the baseline
Most characters in a regex just match themselves. The pattern cat matches the literal text "cat" wherever it appears. That's the easy 80%. The interesting part is the small set of characters that mean something special — . * + ? ^ $ ( ) [ ] { } | \ — and how combining them gives you real matching power.
The dot, and the quantifiers
. matches any single character except a newline. c.t matches "cat", "cut", "c9t" — anything with exactly one character between c and t.
Quantifiers control how many times the preceding thing can repeat:
*— zero or more times.ab*cmatches "ac", "abc", "abbbbc".+— one or more times.ab+cmatches "abc", "abbc", but not "ac".?— zero or one time (optional).colou?rmatches both "color" and "colour".{n,m}— between n and m times.\d{6}matches exactly six digits.
Character classes — matching a set of options
[abc] matches any one of a, b, or c. [a-z] matches any lowercase letter using a range. [^abc] — the caret inside brackets negates it — matches any character except a, b, or c.
This is where shorthand classes save you typing: \d is shorthand for [0-9] (any digit), \w is shorthand for [A-Za-z0-9_] (any "word" character), and \s matches any whitespace (space, tab, newline). Their uppercase versions — \D, \W, \S — match the opposite.
Anchors — pinning a match to a position
^ anchors a match to the start of the string (or line, in multiline mode). $ anchors to the end. This matters more than it looks: the pattern \d{6} matches six digits anywhere in a string, including inside a 10-digit number. The pattern ^\d{6}$ only matches if the entire string is exactly six digits — nothing before, nothing after. This distinction is the single most common source of "my regex matches things it shouldn't" bugs.
Groups — capturing and organizing
Parentheses () create a group — useful for applying a quantifier to a sequence, and for extracting a specific piece of a match. (ab)+ matches "ab", "abab", "ababab" — the quantifier applies to the whole group, not just the last character. In most regex engines, each group you capture is also available afterward — group 1 is the first (...), group 2 the second, and so on, which is how you pull a specific sub-piece out of a larger match (like extracting just the area code from a phone number).
Real example 1 — a practical email-ish pattern
Nobody should write a fully RFC-5322-compliant email regex by hand (it's absurdly complex and mostly pointless — just send a verification email instead). But a reasonable, practical pattern looks like this:
^[\w.+-]+@[\w-]+\.[a-zA-Z]{2,}$
Breaking it down: ^[\w.+-]+ — one or more word characters, dots, plus signs, or hyphens (covers "ravi.kumar+test") anchored to the start. @ — a literal at-sign. [\w-]+ — the domain name part. \. — a literal dot (escaped, because an unescaped . means "any character"). [a-zA-Z]{2,}$ — the TLD, at least two letters, anchored to the end.
Real example 2 — extracting an Indian phone number
Indian mobile numbers are 10 digits, often written with a +91 prefix and sometimes with spaces or hyphens. A pattern to find one inside a larger string of text:
(?:\+91[\s-]?)?[6-9]\d{9}
(?:\+91[\s-]?)? — an optional, non-capturing group for an optional +91 prefix with an optional space or hyphen after it. [6-9] — Indian mobile numbers always start with 6, 7, 8, or 9. \d{9} — the remaining nine digits. This correctly matches "9876543210", "+91 9876543210", and "+91-9876543210", while rejecting a random 10-digit number starting with 0-5 (which isn't a valid Indian mobile prefix).
Real example 3 — validating an Indian PIN code
Indian postal PIN codes are exactly 6 digits, and the first digit is never 0:
^[1-9][0-9]{5}$
Anchored on both ends so it validates the whole input rather than matching a 6-digit substring inside a longer number, [1-9] rules out a leading zero, and [0-9]{5} covers the remaining five digits.
Greediness — the quantifier gotcha that trips everyone up eventually
By default, quantifiers are "greedy" — they match as much as possible while still allowing the overall pattern to succeed. Given the string <b>bold</b>, the pattern <.+> doesn't match just <b> — it greedily matches all the way to the last > in the string, capturing <b>bold</b> entirely, because .+ tries to consume as much as it can first and only backs off if it has to.
Adding a ? after a quantifier makes it "lazy" (non-greedy) — it matches as little as possible instead. <.+?> against the same string stops at the first > it finds, matching just <b>. This single character — a ? after * or + — is the fix for probably the most common "why did my regex match too much" question on Stack Overflow.
Try these live
Reading regex patterns is one thing — building intuition for them takes actually running them against real text and watching what matches. Our Regex Tester gives you live match highlighting as you type, so you can paste in a block of text, try the PIN code or phone number patterns above, and see exactly which substrings match in real time, along with capture group details for each match.