Matching Basics

Regular Expressions in Practice ๐Ÿ†

Chapter 1 ยท Matching Basics

A regular expression (regex) is a small pattern language for describing text: "four digits, a dash, two digits", "a word that starts with a capital", "anything between angle brackets". Instead of writing a loop that inspects characters one at a time, you describe the shape of what you want and let the engine find it. This course is practical: each chapter takes one idea, runs it, and ends with a rule you can reuse. All examples use Python 3 and its built-in re module.

What was run
Every snippet in this chapter was executed, and the output shown in the comments is what Python printed. Run them yourself in a file or the interactive prompt; there is nothing to install.

Three Ways to Ask "Does It Match?"

The re module has three entry points that differ only in where the pattern is allowed to match. Picking the wrong one is the most common beginner mistake, so learn the difference first.

FunctionWhere it looksReturns
re.searchanywhere in the stringfirst match, or None
re.matchonly at the start of the stringmatch, or None
re.fullmatchthe whole string, start to endmatch, or None
re.findalleverywherea list of every non-overlapping match
import re text = "Order 4821 shipped on 2026-11-02 to bay 7." print(re.search(r"\d+", text)) # <re.Match object; span=(6, 10), match='4821'> print(re.match(r"\d+", text)) # None (the text starts with "Order", not a digit) print(re.fullmatch(r"\d+", "4821")) # <re.Match object; span=(0, 4), match='4821'> m = re.search(r"\d+", text) print(m.group(), m.start(), m.end(), m.span()) # 4821 6 10 (6, 10)

A successful call returns a match object; a failed one returns None. Because None is falsy, if re.search(...): is the usual test. On the match object, .group() is the matched text and .span() gives the start and end positions.

Raw Strings: Always Write r"..."

Regex uses the backslash for things like \d (a digit). Python strings also use the backslash for escapes like \n. If you write the pattern as an ordinary string, Python processes the backslashes first and can change your meaning. A raw string, written with an r prefix, leaves every backslash alone and hands it to the regex engine untouched.

print(len("\n"), len(r"\n")) # 1 2 (one newline character versus a backslash and an n) print(re.search("\\d+", "a12").group()) # 12 (works, but the doubled backslash is easy to get wrong) print(re.search(r"\d+", "a12").group()) # 12 (the same pattern, readable)

The Building Blocks

Most patterns are assembled from a few kinds of piece. Most characters simply match themselves; a handful are special.

PieceMeaningExample
\d \w \sa digit; a letter, digit or underscore; whitespace\d\d matches 42
.any one character except a newlinec.t matches cat, cot
[abc]one character from the set[aeiou] matches a vowel
[^abc]one character not in the set[^0-9] matches a non-digit
* + ?zero or more; one or more; zero or onecolou?r matches both spellings
{n} {n,m}exactly n; between n and m repeats\d{4} matches four digits
^ $start of the string; end of the string^cat$ matches only cat
\ba word boundary (between a word character and a non-word one)\bcat\b matches the word, not concat
print(re.findall(r"\d+", text)) # ['4821', '2026', '11', '02', '7'] print(re.findall(r"\d{4}-\d{2}-\d{2}", text)) # ['2026-11-02'] print(re.findall(r"[aeiou]", "regular")) # ['e', 'u', 'a'] print(re.findall(r"[^aeiou\s]+", "regular expressions")) # ['r', 'g', 'l', 'r', 'xpr', 'ss', 'ns']

Note the last line: [^aeiou\s]+ means "one or more characters that are neither a vowel nor whitespace", so the engine returns each run of consonants it finds.

Anchors and Word Boundaries

Anchors match a position, not a character. Without them, a pattern is happy to match inside a longer word, which is rarely what you want.

for s in ["cat", "concat", "cats"]: print(s, bool(re.search(r"^cat$", s)), bool(re.search(r"\bcat\b", s)), bool(re.search(r"cat", s))) # cat True True True # concat False False True # cats False False True

Greedy Versus Lazy

Quantifiers such as * and + are greedy: they grab as much text as they can while still letting the overall pattern succeed. Adding a ? after the quantifier makes it lazy, taking as little as possible. The classic trap is matching tags.

html = "<b>bold</b> and <i>italic</i>" print(re.findall(r"<.*>", html)) # ['<b>bold</b> and <i>italic</i>'] one huge match print(re.findall(r"<.*?>", html)) # ['<b>', '</b>', '<i>', '</i>']

The greedy .* ran to the very last > in the string. The lazy .*? stopped at the first one. Often a better fix than laziness is a more precise class, such as <[^>]*>, which says "anything except a closing bracket".

A habit worth forming
Choose the function by the question you are asking: fullmatch to validate a whole value, search to find something inside text, findall to collect every occurrence. Always write the pattern as a raw string.

Challenges

Exercise 1

Given log = "disk 87% full, cpu 5%, mem 100%", write one findall call that returns only the numbers (['87', '5', '100']) and a second that returns them with the percent sign (['87%', '5%', '100%']). Hint: the first can use a lookahead, (?=%), which checks that a percent sign follows without including it.

๐Ÿ“„ View solution
Exercise 2

Write is_code(s) that returns True only for a product code made of exactly three capital letters, a dash and four digits (such as ABC-1234). Test it on "AB-1234", "ABC-12345", "abc-1234" and "xABC-1234". Then show what re.match does with "ABC-12345" and explain why fullmatch is the right choice.

๐Ÿ“„ View solution
Exercise 3

For tags = "<p>one</p><p>two</p>", show that <p>.*</p> returns one big match. Fix it twice: once with a lazy quantifier and once with a negated class. Predict the output before you run each.

๐Ÿ“„ View solution

Quick Reference

  • search anywhere; match at the start; fullmatch the whole string; findall every match
  • Failure: returns None, so test with if
  • Always write patterns as raw strings: r"\d+"
  • Classes: \d \w \s, sets [abc], negated sets [^abc]
  • Repeats: * + ? {n,m} are greedy; add ? for lazy
  • Positions: ^ $ \b match places, not characters
Coming next
Regular Expressions in Practice 2 introduces groups: capturing parts of a match, naming them, and using them to pull a date or an email address apart into its pieces.