AlgoMaster Logo

Regular Expressions

High Priority17 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

A regular expression (regex) is a small pattern language for matching text. Instead of writing twenty if/in checks to find every order ID in a customer-support message, you describe what an order ID looks like ("the letters ORD followed by six digits") and let the regex engine find every match. This lesson is a surface tour: enough to read and write basic patterns.

Why Regex Exists

String methods take you far. .startswith(), .endswith(), .find(), .split(), .replace() all do real work without any new syntax. They start to fall apart the moment a pattern has any flexibility. Suppose a customer writes:

You want both order IDs. With string methods alone, you're writing a loop, slicing characters, checking each one against the digits 0-9, building up a list of matches. It works. It's tedious, and it gets worse the moment the format changes.

A regex says the whole thing in one line:

The pattern ORD\d{6} reads as "the literal letters ORD, followed by exactly six digits". The engine scans the string and pulls out every spot that matches. That's the value of regex: a compact way to describe a shape of text and find every place it appears.

The trade-off is that regex syntax is dense. A pattern that looks like a cat walked across the keyboard often does something useful, but only after you learn the alphabet. This lesson gives you that alphabet.

import re and the Two Entry Points

Python's regex support lives in the standard-library re module. You import it once at the top of the file:

The two functions you'll use most in this lesson are re.search() and re.match(). They look similar, and the difference trips up most beginners on day one.

re.search(pattern, text) scans the entire string looking for the first place the pattern matches. It can match anywhere in the string.

re.match(pattern, text) only checks whether the pattern matches at the start of the string. If the start doesn't match, it returns None, even if the pattern would match later.

re.search finds HP-450 at position 34 and returns a match object. re.match returns None because the description doesn't start with HP-. If you want to check "does this whole string look like a product code", re.match (or re.fullmatch, which requires matching the entire string) is the right tool. If you want to find a pattern anywhere inside a longer piece of text, use re.search.

re.findall() for All Matches

re.search and re.match give you at most one match. re.findall(pattern, text) returns a list of every non-overlapping match in the string. If nothing matches, you get an empty list, not None.

The pattern [A-Z]{3}-\d{3} says "three uppercase letters, a hyphen, three digits". findall walks left-to-right, pulling out every chunk that matches. The order of the list is the order they appeared in the text.

findall is the right tool when you want to count or collect all occurrences. Counting how many times a customer mentioned an order ID, listing every email address in a support ticket, finding every price in a product description: all findall jobs.

Note the backslash before the $. That's because $ has a special meaning in regex (it anchors to the end of a line, covered below). To match a literal dollar sign, you escape it with a backslash.

Match Objects

When re.search or re.match succeeds, it returns a match object, not a string. When it fails, it returns None. You almost always want to handle both cases:

Two things to notice. First, the match object is truthy, so if result: works as a "did it match" check. None is falsy, so the else branch runs only when there's no match. Second, result.group() returns the matched substring as a regular Python string. Without calling .group(), you'd be holding the match object itself, not the text it found.

Match objects also tell you where in the string the match happened. .start() gives the start index, .end() gives the end index (exclusive), and .span() returns both as a tuple:

That's enough match-object basics for this lesson. Capturing groups let .group(1), .group(2), and so on pull out specific parts of a match, and named groups extend this further.

Pattern Cheatsheet

The patterns in this section are the ones you'll see in almost every regex you ever write. Read it once now to get a feel; you'll come back to it as you work through examples.

PatternMeaningExample patternSample match
a, b, 1A literal charactercartcart
.Any single character except newlinec.tcat, cut, c4t
\dA digit (0-9)\d\d42, 07
\DA non-digit\Da, $,
\wA word char (letter, digit, underscore)\w+order_42
\WA non-word char\W$, -,
\sWhitespace (space, tab, newline)\s+
\SNon-whitespace\S+ORD123
\bWord boundary (zero-width)\bcart\bcart (whole word)
*Zero or more of the previousa*``, a, aaaa
+One or more of the previous\d+1, 42, 9999
?Zero or one of the previouscolou?rcolor, colour
{n}Exactly n of the previous\d{6}123456
{n,m}Between n and m of the previous\d{2,4}42, 9999
^Start of string (or line)^OrderOrder at start
$End of string (or line)shipped$shipped at end
[abc]Any one of a, b, or c[aeiou]a, e, ...
[a-z]Any character in the range[A-Z]+JBL, ABC
[^abc]Any character not in the set[^0-9]a, $,
a|ba or bcat|dogcat or dog

A few things to keep in mind:

The character classes (\d, \w, \s) and their uppercase opposites (\D, \W, \S) cover most of what you need. \d matches 0 through 9. \w matches the same characters that Python uses for identifiers: letters, digits, and underscore. \s matches any whitespace, including spaces, tabs, and newlines.

Quantifiers (*, +, ?, {n,m}) attach to whatever comes immediately before them. \d+ means "one or more digits", not "one digit followed by something". To repeat a longer pattern you'd group it with parentheses.

^ and $ are anchors: they don't match a character, they match a position. ^Order doesn't consume the letter O, it just asserts "the next thing must be at the start of the string". The same goes for \b, which marks the boundary between a word character and a non-word character.

Character classes in square brackets are a small mini-language of their own. [abc] matches any one of those three letters. [a-z] is the range a through z. [^abc] is everything except a, b, or c (the ^ inside [] means negation, totally different from the anchor ^ outside).

Why Raw Strings Matter for Regex

You may have noticed every pattern so far is written with an r prefix: r"\d+", not "\d+". That's a raw string literal. Here's the short version of why it matters for regex.

Python's normal string syntax treats backslash as an escape character. "\n" is a newline, "\t" is a tab, "\\" is a single backslash. Regex syntax also uses backslash for its own escapes: \d, \w, \s, \b. When you write "\d" without the raw prefix, Python doesn't recognize \d as a known escape, so it leaves it alone (today; this is a DeprecationWarning and may break in future Python versions). For "\b", Python turns it into the ASCII backspace character, not a regex word boundary, and your pattern silently breaks.

The first call passes the regex engine a pattern with two literal backspace characters around cart, which never matches a normal sentence. The second call passes the four characters \, b, c, a, r, t, \, b to the engine, which interprets \b as "word boundary" and finds the standalone word cart.

The rule is simple: always use a raw string (`r"..."`) for regex patterns. Even when the pattern doesn't contain a backslash. It costs you one character and prevents an entire category of bugs.

A Worked E-Commerce Example

Let's pull all of this together. A customer-support inbox might contain messages like this one:

There's a lot in there. Let's extract specific pieces, one at a time.

Find all order IDs

ORD\d{6} says "the literal text ORD followed by exactly six digits".

Find all product codes

Product codes look like BRD-7821: three uppercase letters, a hyphen, four digits.

The character class [A-Z]{3} matches three uppercase letters. The hyphen is just a literal. \d{4} matches four digits.

Roughly validate the email

A full email regex is famously hard to get right (the official spec is hundreds of lines). For most everyday checks, a forgiving pattern is fine: "some non-whitespace, an at sign, more non-whitespace, a dot, more non-whitespace".

\S+ is one or more non-whitespace characters. The @ and . are literals (the . is escaped because we want a literal dot, not "any character"). This will accept things that aren't really emails (like a@b.c), but for sorting customer messages it's good enough.

Find a phone number

The number 555-867-5309 is three digits, hyphen, three digits, hyphen, four digits.

Here's a small Mermaid view of what \d{3}-\d{4} would match (a simpler four-digit suffix), so you can see the engine as a step-by-step state machine:

The regex engine moves through these states left to right, advancing through the input as it goes. If any state fails, the engine backs up and tries again from the next position.

Find the refund amount

Five extractions, five regex calls, no manual character scanning. That's the win.

Common Pitfalls

Two pitfalls bite beginners often enough to mention up front.

Quantifiers are greedy by default

+, *, and {n,m} are greedy: they grab as much text as they possibly can while still allowing the rest of the pattern to match. Sometimes that's not what you want.

You probably wanted three matches: <b>, </b>, <b>, </b>. Instead you got one giant match that swallows almost the entire string. That's because .+ greedily ate everything from the first < all the way to the last >, since the pattern still matches that way.

The fix is the lazy version of the quantifier: +? and *? match as little as they can.

The ? after + flips it from greedy to lazy.

. does not match newlines

The wildcard . matches any character except the newline character \n, by default. Patterns that span lines often surprise people:

Empty list. The \n between product and Fast doesn't match .. To make . include newlines, you pass the re.DOTALL flag:

Flags like re.DOTALL, re.IGNORECASE, and re.MULTILINE change how the engine interprets the pattern.

You have enough to read most regex patterns you'll see in the wild and write the patterns most string-processing tasks need.

Quiz

Regular Expressions Quiz

10 quizzes