You have a folder full of files named invoice_2026_01.pdf, invoice_2026_02.pdf, and a hundred more. You need the ones from the first half of the year, but not the drafts, and definitely not the invoice_2026_final_FINAL.pdf someone left in there. You could click through them one by one. Or you could describe the pattern you're looking for and let the computer find every match in a fraction of a second.

That's what regular expressions do. A regex is a small language for describing patterns in text, and it's built into nearly every tool a developer touches — your editor's search box, grep, Python, JavaScript, SQL, log analyzers, form validators. Learn it once and you carry it everywhere.

The catch is that regex has a reputation for looking like a cat walked across the keyboard. ^\d{4}-\d{2}-\d{2}$ is genuinely intimidating the first time you see it. But the syntax is small, and once you can read it left to right the mystery evaporates. This guide gets you to that point.

A regular expression isn't magic. It's a compact way of saying "text that looks like this" — and the vocabulary fits on a postcard.

The five pieces that cover 90% of real use

Almost everything you'll write combines just five ideas: literal characters, character classes, quantifiers, anchors, and groups. Get comfortable with these and the rest is decoration.

Literals are the easy part. The pattern cat matches the letters c-a-t in that order, anywhere in the text. cat matches inside "category" and "scatter" too, which surprises beginners — a regex matches a substring unless you tell it otherwise.

Character classes describe a set of allowed characters using square brackets. [aeiou] matches any one vowel. [0-9] matches any single digit, and [a-z] any lowercase letter. There are shorthands you'll use constantly: \d is a digit (same as [0-9]), \w is a "word" character (letters, digits, underscore), and \s is whitespace. Their uppercase versions negate them — \D is "any non-digit". A dot . is the wildcard: it matches any character at all, which is powerful and, as we'll see, occasionally too eager.

Quantifiers: saying "how many"

A character class matches exactly one character. Quantifiers let you say how many of the preceding thing you want, and this is where regex starts feeling useful instead of tedious.

The three you'll reach for daily are * (zero or more), + (one or more), and ? (zero or one, i.e. optional). So \d+ means "one or more digits" — perfect for pulling a number out of text. colou?r matches both "color" and "colour" because the u is optional. When you need an exact count, use braces: \d{4} matches exactly four digits, \d{2,4} matches between two and four.

Here's the same date pattern built up piece by piece so it stops looking scary:

import re

text = "Order placed on 2026-07-27, ships 2026-08-03."

# \d{4} = four digits, then a literal dash, then two digits, etc.
pattern = r"\d{4}-\d{2}-\d{2}"

print(re.findall(pattern, text))
# ['2026-07-27', '2026-08-03']

Notice the r"..." prefix on the string. That's a raw string, and you should use it for every regex in Python. Without it, the backslash in \d gets interpreted by Python before the regex engine ever sees it, and you'll spend an afternoon confused about why nothing matches. Most languages have an equivalent gotcha around backslashes.

Anchors: matching position, not characters

Anchors are strange at first because they match a position rather than a character — they take up zero width. The two you need are ^ for "start of the string" and $ for "end of the string".

Why does this matter? Remember that cat matches inside "scatter". If you're validating that a user typed exactly a four-digit year and nothing else, \d{4} isn't enough — it happily matches the "2026" inside "x2026y". Wrapping it as ^\d{4}$ says "from the start to the end, exactly four digits, nothing before or after." This distinction is the single most common source of validation bugs I see in code review: a pattern that's technically correct but unanchored, so it passes input it should reject.

import re

def is_year(s):
    return bool(re.match(r"^\d{4}$", s))

print(is_year("2026"))     # True
print(is_year("x2026y"))   # False — anchors reject the junk
print(is_year("20261"))    # False — five digits, no match

Groups and alternation: capturing what you want

Parentheses do two jobs. First, they group part of a pattern so a quantifier applies to the whole group: (ab)+ matches "ababab". Second, and more usefully, they capture the matched text so you can pull it out afterward.

Say you want to extract the year and month from those filenames. Put parentheses around the parts you care about, and the engine hands them back to you separately:

import re

filename = "invoice_2026_07.pdf"
m = re.search(r"invoice_(\d{4})_(\d{2})", filename)

if m:
    print(m.group(1))  # '2026'  — first captured group
    print(m.group(2))  # '07'    — second captured group

The vertical bar | means "or". (jpg|png|gif) matches any of the three. Combine it with the pieces above and you can write a pattern that finds every image reference in a document, or splits a CSV row that a naive split(",") would mangle.

Where regex is the wrong tool

For all its power, regex has a famous failure mode, and knowing it saves you real pain. Don't use regex to parse nested or deeply structured formats — HTML, JSON, or a full programming language. These formats can nest inside themselves to arbitrary depth, and regex fundamentally cannot count matching brackets. Use a real parser (an HTML library, json.loads, and so on). The Stack Overflow answers screaming "don't parse HTML with regex" are, for once, correct.

The other trap is the greedy wildcard. By default quantifiers are greedy.* grabs as much as it possibly can. In <a>one</a><a>two</a>, the pattern <a>.*</a> matches the entire string, not just the first tag, because .* swallows everything up to the last </a>. Add a ? to make it lazy — <a>.*?</a> — and it stops at the first closing tag. Then remember the paragraph above and reach for a parser anyway.

How to actually get good at this

You don't memorize regex; you build it incrementally and test as you go. Open a site like regex101.com, paste in a sample of your real text, and grow the pattern one piece at a time, watching what lights up. Start with the literal parts, add character classes, then quantifiers, then anchor it. When something doesn't match, comment out the last thing you added and you've found the culprit.

Keep a few workhorses in your back pocket. \s+ collapses runs of whitespace. ^\s*$ finds blank lines. \b is a word boundary, so \bcat\b matches "cat" as a whole word but not inside "category". And when a pattern grows past a line or two, most engines let you write it in verbose mode with comments — a future reader (usually you) will be grateful.

Regular expressions reward a small upfront investment with a tool you'll use for the rest of your career. The syntax that looks like line noise today becomes readable within a week of deliberate practice. Start with the five pieces, anchor your validators, and hand the nested stuff to a proper parser. The next time you're staring down a folder of a thousand files, you'll describe what you want in a dozen characters and move on with your day.