There’s a famous joke that every programmer knows: “Some people, when confronted with a problem, think ‘I know, I’ll use regular expressions.’ Now they have two problems.” When I first encountered regex, I believed it. The patterns looked like someone had fallen asleep on their keyboard: ^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$. I avoided them for a year, writing clumsy string-splitting code instead.
That was a mistake. This regular expressions tutorial will show you that regex is not black magic — it’s a small, learnable language for describing text patterns, and it pays for itself within weeks. Validating emails, extracting phone numbers, cleaning messy data, searching codebases: regex does in one line what takes dozens without it. By the end of this guide, you’ll read patterns like the one above fluently — and write your own.

What Regular Expressions Actually Are
A regular expression (regex) is a pattern that describes a set of strings. You give the regex engine a pattern and some text; it tells you whether (and where) the text matches. That’s the whole concept. Everything else is vocabulary.
The key realization: you’re already doing pattern matching mentally. When you scan a document for email addresses, your brain looks for “some text, then @, then more text, then a dot, then a few letters.” Regex just makes that instinct explicit and executable:
[\w.-]+@[\w.-]+\.\w+
Read it piece by piece: [\w.-]+ means “one or more word characters, dots, or hyphens” (the username), @ is a literal @, [\w.-]+ again (the domain), \. is a literal dot, \w+ is the extension. Each symbol is simple; the power is in combining them.
Regex works in virtually every language — Python, JavaScript, Java, Go, Rust — plus editors, command-line tools (grep), and databases. Learn it once, use it everywhere. That’s a rare bargain in programming.
Regular Expressions Tutorial: The Core Vocabulary
Regex has a small core. Master these and you can read most real-world patterns:
Literal characters match themselves: cat matches “cat” in “concatenate.”
Character classes [...] match one of the listed characters: [aeiou] matches any vowel. Ranges work: [a-z], [0-9], [A-Za-z].
Shorthand classes (the ones you’ll use constantly):
– \d — any digit (same as [0-9])
– \w — word character: letters, digits, underscore
– \s — whitespace (spaces, tabs, newlines)
– . — any character except newline (use carefully!)
Capital versions negate: \D is non-digit, \W non-word, \S non-whitespace.
Quantifiers say how many:
– * — zero or more
– + — one or more
– ? — zero or one (optional)
– {n} — exactly n; {n,} — n or more; {n,m} — between n and m
Anchors pin the match in place:
– ^ — start of string (or line)
– $ — end of string (or line)
– \b — word boundary
Let’s combine them into something useful — a US phone number pattern:
\d{3}-\d{3}-\d{4}
\d{3} matches exactly three digits, then a literal hyphen, three more digits, hyphen, four digits. It matches “555-123-4567” and rejects “55-1234-567.” One line, precise. Try reading it aloud: “three digits, dash, three digits, dash, four digits.” If you can say it, you can write it.
Testing Regex: The Feedback Loop That Makes It Learnable
Regex has a reputation for being write-only, but that’s because people try to learn it from tables instead of experimenting. The secret: use a visual regex tester. Sites like regex101.com let you type a pattern and test text side by side, with each match highlighted live and every token of your pattern explained in plain English.
This changes everything. Instead of guessing whether \d{3} does what you think, you see it match instantly. When I finally learned regex properly, it was an afternoon with a tester and a list of small challenges — not a tutorial. Do this:
- Open a regex tester.
- Paste some messy text (an address block, a log file, a paragraph).
- Try to match just the email addresses. Then just the phone numbers. Then just the dates.
- For each attempt, read the tester’s explanation of what your pattern actually means.
An hour of this builds more intuition than a week of reading. Regex is a doing skill; the tester is your gym.
Practical Patterns You’ll Actually Reuse
Theory is nice; reusable patterns pay rent. Here are patterns I reach for constantly, each annotated so you can adapt them:
Email (pragmatic version):
[\w.-]+@[\w.-]+\.\w{2,}
Caveat: no regex validates emails perfectly (the true spec is absurdly complex). This catches real addresses and rejects obvious junk — good enough for client-side sanity checks. Server-side, send a confirmation email; that’s the only real validation.
URL:
https?://[\w.-]+(?:\.[\w.-]+)+[\w\-._~:/?#\[\]@!$&'()*+,;=]*
The (?:...) is a non-capturing group — it groups without remembering. Don’t memorize this one; understand its shape (protocol, domain, path characters) and adapt.
Date (YYYY-MM-DD):
\d{4}-\d{2}-\d{2}
Simple and strict about format. (Validating real calendar dates — no February 30th — needs code, not regex. Know the boundary.)
Extracting hashtags:
#\w+
In Python:
import re
text = "Learning #python and #regex today!"
tags = re.findall(r"#\w+", text)
print(tags) # ['#python', '#regex']
Note the r prefix — raw strings keep backslashes literal, so r"\d" is backslash-d, not an escape sequence. Always use raw strings for regex in Python; forgetting this is the classic beginner bug.
Capture Groups: Extracting, Not Just Matching
Matching tells you whether text fits. Capture groups tell you which parts matched — and that’s where regex becomes a data-extraction tool. Parentheses create groups:
(\d{4})-(\d{2})-(\d{2})
Applied to “2026-10-14”, group 1 is “2026”, group 2 is “10”, group 3 is “14.” In Python:
import re
match = re.search(r"(\d{4})-(\d{2})-(\d{2})", "Posted on 2026-10-14 by admin")
if match:
year, month, day = match.groups()
print(f"Year: {year}, Month: {month}, Day: {day}")
Groups also power find-and-replace with rearrangement — the killer feature in editors. In VS Code’s search-and-replace (regex mode on), find (\w+)@(\w+) and replace with $2@$1 to swap username and domain. I use this constantly for reformatting data: “Last, First” → “First Last” is (\w+), (\w+) → $2 $1. My best code editor guide covers which editors have the strongest regex find-and-replace.
Named groups make complex patterns readable:
pattern = r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})"
match = re.search(pattern, "2026-10-14")
print(match.group("year")) # 2026
When a pattern has more than two groups, names beat numbers for clarity.

The Traps: Greediness, Escaping, and Overuse
Three gotchas account for most regex suffering:
Greedy matching. By default, quantifiers grab as much as possible. Applied to "<b>hi</b> <b>bye</b>", the pattern <.*> matches the entire string — .* swallows everything between the first < and the last >. The fix: make it lazy with ? — <.*?> matches each tag separately. When a pattern matches “too much,” laziness is almost always the answer.
Forgetting to escape. Characters like ., *, +, ?, (, ), [, ], {, }, ^, $, |, and \ are special. To match a literal dot (as in a domain), write \.. To match a literal backslash, write \\. When a pattern mysteriously matches everything, check your escaping first.
Using regex where it doesn’t belong. The famous Stack Overflow answer puts it well: don’t parse HTML with regex — HTML is nested and irregular, regex is for regular patterns. Use a proper parser (BeautifulSoup, DOM methods). Similarly, don’t validate complex formats (emails, URLs) with thousand-character patterns when a simple check plus real verification works better. Regex is a scalpel; not every text problem is surgery. For structured data tasks, Python for data analysis tools like pandas string methods (which accept regex!) are often cleaner.
Regex Across Languages: Same Patterns, Different Wrappers
The pattern syntax is nearly universal; only the API differs:
Python:
import re
re.search(r"\d+", "Order 42") # find first match
re.findall(r"\d+", "a1 b22 c333") # find all → ['1', '22', '333']
re.sub(r"\d+", "#", "a1 b2") # replace → 'a# b#'
JavaScript:
"Order 42".match(/\d+/); // find first match
"a1 b22".match(/\d+/g); // find all (g flag)
"a1 b2".replace(/\d+/g, "#"); // replace all
Note JavaScript uses /pattern/flags literal syntax — no raw strings needed, but forward slashes inside the pattern must be escaped (\/). The g flag means “global” (find all, not just first); i means case-insensitive.
The patterns transfer directly; only the function names change. This universality is why regex repays learning more than most language-specific tricks.
Regex on the Command Line: grep, sed, and Friends
Everything so far assumed code or an editor — but regex shines brightest in the terminal, where it turns thousand-file codebases into searchable haystacks. Three tools cover most needs:
grep finds lines matching a pattern, recursively across directories:
# Find every TODO comment in the project
grep -rn "TODO" src/
# Find function definitions in Python files (extended regex with -E)
grep -rnE "^def \w+" src/
# Case-insensitive search for "password" (auditing for hardcoded secrets)
grep -rni "password" src/ --include="*.py"
The -r recurses, -n shows line numbers, -E enables extended regex, -i ignores case. I probably run a grep variant twenty times a day — “where is this function called?”, “which files mention this API?”, “did I leave any console.log statements?” It’s the fastest code navigation that exists, and it works in any project without configuration.
sed edits with regex — most famously find-and-replace across files:
# Replace all occurrences of "http://" with "https://" in every .js file
sed -i 's|http://|https://|g' src/*.js
(Note the | delimiters instead of / — handy when the pattern itself contains slashes.)
ripgrep (rg) is grep’s modern successor: same idea, dramatically faster, respects your .gitignore automatically. If you find yourself grepping large repos, install it — the speedup on big codebases is startling.
One caution carried over from the editor world: these tools operate on raw text, so the same “don’t parse HTML with regex” rule applies. But for searching code, logs, and config files, command-line regex is a genuine superpower — and it pairs beautifully with the Git skills from my Git vs GitHub guide, since you’ll often search the history of a codebase, not just its current state.
A Practice Plan That Actually Works
- One tester session (1 hour): work through matching emails, phones, dates, and hashtags in sample text. Read every explanation the tester gives.
- Learn groups (30 min): extract parts from dates and reformat “Last, First” names with find-and-replace in your editor.
- Use it in real code (ongoing): next time you need to validate input, extract data from text, or clean strings, reach for regex first. Real need cements learning like nothing else.
- Study one gnarly pattern (20 min): find a real-world regex (a URL validator, a log parser), and decode it token by token with the tester’s explainer. Decoding builds reading fluency.
- Learn the limits (ongoing): each time regex feels painful, ask whether a parser or simple string methods would be cleaner. Judgment about when is the advanced skill.
Most developers go from regex-fearful to regex-comfortable in a few focused sessions plus regular real-world use. You won’t memorize every token — nobody does — but you’ll know the core cold and know how to look up the rest.

Power Tools: Lookarounds, Backreferences, and Flags
Once the core vocabulary feels natural, a second tier of features unlocks the patterns that separate casual users from regex-confident developers. These look intimidating; each is one clear idea.
Lookahead and lookbehind: match with conditions. Sometimes you want to match something only if it’s followed (or preceded) by something else — without including that something in the match. “Find prices, but only the dollar amounts, not the $ sign”:
(?<=\$)\d+
(?<=\$) is a lookbehind: it asserts “a $ comes right before this position” without consuming it. Applied to “Price: $49”, the match is “49” — the $ isn’t part of the result. Lookahead works the same way forward: \d+(?= dollars) matches digits only when followed by ” dollars”. The negative versions ((?!...) and (?<!...)) assert absence: \d+(?! dollars) matches numbers not followed by ” dollars”.
I use lookarounds constantly for extraction: pulling usernames from “@user” mentions ((?<=@)\w+ gives “user” without the @), or finding words not inside HTML tags. They’re the answer whenever you think “match X, but only in context Y.”
Backreferences: match what you already matched. \1 refers back to what group 1 captured. The classic use: finding doubled words.
\b(\w+)\s+\1\b
This matches “the the” or “had had” — group 1 captures a word, \1 demands the same word again. Proofreaders automate typo-hunting with this. Backreferences also validate symmetric formats: (['"]).*?\1 matches a quoted string where the closing quote must match the opening one (single with single, double with double).
Alternation: this OR that. The pipe | tries alternatives left to right:
https?://(www\.)?(google|bing|duckduckgo)\.com
Matches google.com, www.bing.com, duckduckgo.com — with or without www, http or https. Keep alternatives ordered with the most specific first when they overlap, and group them with parentheses so the | doesn’t leak into surrounding pattern.
Flags that change the rules. Appended to the pattern (or passed as options), flags modify matching globally:
– Case-insensitive (i): [a-z] matches uppercase too. In Python: re.compile(r"hello", re.IGNORECASE).
– Multiline (m): ^ and $ match at line starts/ends, not just string ends — essential for processing multi-line text.
– Dotall (s): . matches newlines too, for patterns spanning lines.
import re
log = "ERROR: disk full\nINFO: ok\nERROR: timeout"
# Find all ERROR lines (multiline ^ anchors to each line start)
errors = re.findall(r"^ERROR:.*", log, re.MULTILINE)
print(errors) # ['ERROR: disk full', 'ERROR: timeout']
Without re.MULTILINE, ^ERROR would only match at the very start of the string. Flags are small annotations with outsized effects — when a pattern inexplicably fails on multi-line input, check your flags first.
Putting it together: a log parser. Real patterns combine everything:
^(?P<date>\d{4}-\d{2}-\d{2}) (?P<level>INFO|WARN|ERROR): (?P<msg>.+)$
With re.MULTILINE, this parses each log line into named groups — date, level, message — in one pass. That’s a genuinely useful tool: feed it a server log, get structured data. Notice how every piece is something from this guide: anchors, named groups, alternation, quantifiers. Complex patterns aren’t a new skill — they’re the basics, composed.
Key Takeaways
- This regular expressions tutorial boils down to: patterns describe text shapes; the engine finds matches.
- Learn the core first: character classes (
\d,\w,\s), quantifiers (*,+,?,{n,m}), anchors (^,$). - A visual regex tester with live highlighting is the fastest way to learn — experiment, don’t just read.
- Capture groups turn matching into extraction; they’re the most valuable intermediate skill.
- Watch for greedy matching (fix with lazy
?), escaping mistakes, and using regex on nested structures like HTML. - Patterns transfer across languages — learn once, use in Python, JavaScript, editors, and the command line.
Frequently Asked Questions
Are regular expressions hard to learn?
The basics are easier than their reputation — a focused afternoon with a visual tester gets you through the core vocabulary. What’s genuinely hard is reading complex patterns others wrote, and that comes with practice. Most developers use a small subset of regex 95% of the time.
Which regex flavor should I learn?
Start with whatever your primary language uses (Python’s re or JavaScript’s syntax). The core syntax is nearly identical across flavors; differences (like lookbehind support) only matter for advanced use. Learning one well means you can read them all.
Can regex validate email addresses perfectly?
No — the email specification allows bizarre edge cases no reasonable pattern covers. Use a pragmatic pattern for basic sanity checks and verify with a confirmation email for anything important. The same applies to URLs and phone numbers: regex for shape, real verification for certainty.
What’s the difference between greedy and lazy matching?
Greedy quantifiers (.*) match as much text as possible; lazy ones (.*?) match as little as possible. On "<b>hi</b> <b>bye</b>", greedy <.*> matches the whole string while lazy <.*?> matches each tag separately. Default to greedy, switch to lazy when matches swallow too much.
Should I use regex to parse HTML?
No. HTML is nested and irregular; regex handles regular patterns. Use a proper HTML parser (like BeautifulSoup in Python or DOM methods in JavaScript). Regex is perfect for the text inside the parsed content, just not for the parsing itself.
That keyboard-smash pattern from the introduction — ^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$ — should now read like a sentence: “start, word characters, an @, domain parts separated by dots, a 2–4 letter ending, done.” That’s the whole journey of this regular expressions tutorial: from intimidating noise to readable notation. Open a tester, grab some messy text, and start matching — your future self, drowning in log files and user input, will thank you.
