Regex for Beginners: A Practical Guide With Real Examples
Regex has a reputation for being unreadable, and a line like ^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$ does nothing to argue against it.
But that reputation comes from how regex is usually encountered: as a finished pattern in someone else's code, with no indication of how it was assembled. Built up piece by piece, the same pattern is straightforward.
There are only five concepts you genuinely need. This guide covers those five, builds three real patterns from nothing, and then covers the one behaviour that trips up nearly every beginner.
The five building blocks
1. Character classes: what kind of character
Square brackets define a set, and any single character from that set matches.
[abc] matches one a, b, or c[a-z] matches any one lowercase letter[0-9] matches any one digit[^0-9] matches any one character that is not a digit, because ^ inside brackets means "not"Several common sets have shorthand: \d is any digit, \w is any letter, digit, or underscore, \s is any whitespace. Their capitalised versions invert them, so \D is any non-digit. And a bare . matches almost any single character at all.
2. Quantifiers: how many
A quantifier applies to whatever came immediately before it.
? means zero or one* means zero or more+ means one or more{3} means exactly three{2,4} means between two and fourSo \d{3} matches exactly three digits, and colou?r matches both British and American spellings, because the u becomes optional.
3. Anchors: where in the string
Anchors match positions rather than characters.
^ at the start of a pattern means "beginning of the string"$ at the end means "end of the string"\b marks a word boundaryThe difference matters enormously. \d{3} finds three digits anywhere in a string, including inside a longer number. ^\d{3}$ matches only if the entire string is exactly three digits and nothing else. Validation almost always wants the anchored version.
4. Groups: treating several characters as one unit
Parentheses bundle characters together so a quantifier applies to the whole bundle, and they capture what matched for later use.
(ab)+ matches "ab", "abab", "ababab". Without the parentheses, ab+ would match "a" followed by one or more "b" characters, which is a completely different pattern.
Groups also extract. Matching (\d{4})-(\d{2}) against "2026-07" gives you "2026" in group one and "07" in group two.
5. Alternation: this or that
The pipe character means or. cat|dog matches either word. Combined with groups, ^(cat|dog)s?$ matches cat, cats, dog, or dogs and nothing else.
→ Use our free Regex Tester at GlobalUtilityHub to try any pattern against your own text and see exactly what it matches. No sign-up needed.
The tokens worth memorising
| Token | Matches | |
|---|---|---|
| `.` | Any single character | |
| `\d` | Any digit | |
| `\D` | Any non-digit | |
| `\w` | Letter, digit, or underscore | |
| `\s` | Any whitespace | |
| `[abc]` | One of a, b, or c | |
| `[^abc]` | Any character except a, b, c | |
| `[a-z]` | Any lowercase letter | |
| `?` | Zero or one | |
| `*` | Zero or more | |
| `+` | One or more | |
| `{n}` | Exactly n | |
| `{n,m}` | Between n and m | |
| `^` | Start of string | |
| `$` | End of string | |
| `\b` | Word boundary | |
| `()`, Group and capture | ||
| `\ | ` | Or |
That table covers the overwhelming majority of patterns in ordinary code.
Three patterns, built from scratch
A UK postcode
UK postcodes have an outward and inward code, and the outward part varies. Build it in stages.
[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}^...$Assembled: ^[A-Z]{1,2}\d[A-Z\d]? ?\d[A-Z]{2}$
Tested, this matches "SW1A 1AA", "M1 1AE", "B33 8TH", and "EC1A1BB" without the space. It rejects "ZZ 999". The line that looked like noise at the top of this article is six decisions in sequence.
Extracting a timestamp from a log line
Given lines like 2026-07-20 14:32:11 ERROR Connection refused, pull out the date and time separately.
(\d{4}-\d{2}-\d{2})(\d{2}:\d{2}:\d{2})Assembled: (\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2})
Group one returns the date, group two the time. No anchors here, because the timestamp sits at the start of a longer line and you want to find it rather than match the whole thing.
Checking a password contains a digit
This one introduces a construct worth knowing: the lookahead, written (?=...). It checks whether something exists ahead in the string without consuming it.
^(?=.*\d).{8,}$ reads as: from the start, assert that somewhere ahead there is a digit, then match at least eight characters of anything, to the end.
Stack lookaheads for multiple rules. ^(?=.*\d)(?=.*[a-z])(?=.*[A-Z]).{8,}$ requires a digit, a lowercase letter, and an uppercase letter, with a minimum length of eight.
Worth saying: for real password policy, length matters more than composition rules, and current NIST guidance discourages mandatory character-class requirements. Use this as a regex example rather than a security recommendation.
Greedy versus lazy: the one that catches everyone
Quantifiers are greedy by default. They take as much as they possibly can and only give characters back if the rest of the pattern fails.
Take the string <b>bold</b> and <i>italic</i> and the pattern <.*>.
You probably expect four matches, one per tag. You get one: the entire string from the first < to the last >. The .* grabbed everything it could, and since the string does end in >, the pattern succeeded without ever backing off.
Add a question mark after the quantifier to make it lazy: <.*?>. Now it takes as little as possible, and returns all four tags separately.
This single character is the difference between a pattern that works and one that quietly swallows half your input. Whenever a pattern matches far more than intended, greedy quantifiers are the first thing to check.
Three ways regex goes wrong
Validating email addresses
The full specification for valid email addresses runs to hundreds of characters as a regex, and even then it will reject addresses that are legitimately deliverable while accepting ones that bounce. A pattern cannot tell you whether an address exists.
Check for an @ with something either side, then send a confirmation email. The email is the actual validation.
Parsing HTML
Regex describes patterns in flat text. HTML is a nested tree, and nesting is a structure regex fundamentally cannot express. Any pattern that appears to parse HTML works only on the specific markup it was tested against and breaks on the first attribute, comment, or nested element it did not anticipate.
Use a parser. Every language has one.
Catastrophic backtracking
This one is a genuine denial of service risk, not just a correctness problem.
Some patterns cause the engine to explore an exponentially growing number of possibilities when a match fails. The classic shape is a quantifier applied to a group that itself contains a quantifier, such as ^(a+)+$.
Tested against a string of a characters followed by one b, so the match must ultimately fail:
| Input length | Time |
|---|---|
| 18 characters | 0.015s |
| 20 characters | 0.108s |
| 22 characters | 0.235s |
| 24 characters | 0.924s |
Each pair of extra characters roughly doubles the work. Extend that curve a little further and a single request hangs a thread indefinitely.
The corrected pattern ^a+$ matches the same strings and completes the 24-character case in 0.00016 seconds, roughly six thousand times faster.
The warning sign is a quantifier wrapping a group that already contains one: (x+)+, (x*)*, (x|y)+ where x and y can match the same text. If user input reaches a pattern like that, it is an attack surface.
Debugging a pattern that will not match
Most regex frustration comes from patterns that look correct and return nothing. Five checks resolve the large majority of cases, roughly in order of how often they are the cause.
1. Is your quantifier greedy when it should be lazy?
Covered above, and it accounts for more surprises than everything else combined. If a pattern matches too much, add ? after the quantifier and re-test.
2. Have you escaped the characters that need it?
Twelve characters have special meaning: . ^ $ * + ? ( ) [ ] { } | and the backslash itself. To match one literally, escape it with a backslash. The most common victim is the dot. 3.14 matches "3x14" because the dot means any character. 3\.14 matches only the literal.
3. Are you anchoring when you should not be, or vice versa?
^ and $ mean the whole string must match. Use them for validation, where extra content should be rejected. Omit them for extraction, where you are hunting for something inside a larger body of text. Applying anchors to an extraction pattern is a silent way to get zero matches.
4. Are your flags right?
Flags change the whole pattern's behaviour, and their absence is easy to overlook.
[A-Z] will not match lowercase without the i flag.^ and $ mean start and end of the entire input. With the multiline flag they mean start and end of each line, which is usually what you want when processing log files.5. Is your input what you think it is?
Before blaming the pattern, print the exact string being tested. Trailing whitespace, a Windows line ending, a non-breaking space pasted from a browser, or a Unicode quotation mark that looks identical to an ASCII one will all defeat a correct pattern. This is worth checking early, because it produces the most baffling failures.
Build in stages. The most reliable method is to write the smallest piece, confirm it matches, then add the next piece and confirm again. A pattern assembled in six verified steps is far easier to fix than one written whole and then debugged backwards. Each of the three patterns earlier in this guide was constructed exactly that way.
The bottom line
Regex is five ideas: character classes for what kind of character, quantifiers for how many, anchors for where, groups for bundling and extraction, and alternation for choice. Nearly every pattern you will meet is a combination of those.
Build patterns in stages and test each stage rather than writing the whole thing and hoping. Remember that quantifiers are greedy unless you add a question mark. And know the three jobs regex should not be given: email validation, HTML parsing, and any pattern with nested quantifiers exposed to user input.
Our Regex Tester shows you exactly what a pattern matches against your own text. Try it free at globalutilityhub.com/dev-tools/regex-tester/
Use our free Regex Tester to apply what you have learned.
Open Regex Tester →