Regular Expressions: A Practical Guide With Examples

Regex isn't black magic. Learn the core pattern vocabulary, capture groups, and reusable real-world patterns with examples you can run.

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.

Close-up of colorful programming code on a computer screen.
Regex patterns work in almost every programming language you’ll learn. (Photo by Markus Spiske on Pexels)

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:

  1. Open a regex tester.
  2. Paste some messy text (an address block, a log file, a paragraph).
  3. Try to match just the email addresses. Then just the phone numbers. Then just the dates.
  4. 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.

A close-up view of a person typing on a laptop keyboard indoors.
Testing regex patterns interactively is the fastest way to learn them. (Photo by Breakingpic on Pexels)

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

  1. One tester session (1 hour): work through matching emails, phones, dates, and hashtags in sample text. Read every explanation the tester gives.
  2. Learn groups (30 min): extract parts from dates and reformat “Last, First” names with find-and-replace in your editor.
  3. 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.
  4. 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.
  5. 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.

Vibrant close-up of code displayed on a monitor with various programming details.
From validation to search-and-replace, regex shows up everywhere in real code. (Photo by Muhammed Ensar on Pexels)

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.

Leave a Reply

Your email address will not be published. Required fields are marked *