Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

Lesson 7 of 10

A regular expression describes a pattern of text. Use them to find, validate, extract, and replace when plain string methods aren't enough.

import re

text = "Contact: ada@example.com or grace@test.org"
emails = re.findall(r"\S+@\S+\.\w+", text)
print(emails)

Raw strings

Always write patterns as r"...". Regex uses backslashes heavily, and so does Python — a raw string stops them fighting:

print(len("\d"))
print(len(r"\d"))

Without the r, Python may interpret the escape before regex ever sees it. Make r"..." a habit.

The building blocks

| Pattern | Matches | | --- | --- | | \d / \D | a digit / a non-digit | | \w / \W | word character (letter, digit, _) / not | | \s / \S | whitespace / non-whitespace | | . | any character except newline | | [abc] / [^abc] | one of these / none of these | | [a-z] | a range |

Quantifiers say how many:

| Pattern | Means | | --- | --- | | * | zero or more | | + | one or more | | ? | zero or one | | {3} / {2,4} | exactly 3 / between 2 and 4 |

Anchors say where:

| Pattern | Means | | --- | --- | | ^ | start of string | | $ | end of string | | \b | word boundary |

The main functions

import re

text = "The year 2026 follows 2025"

print(re.search(r"\d{4}", text).group())
print(re.findall(r"\d{4}", text))
print(re.sub(r"\d{4}", "YEAR", text))
print(re.split(r"\s+", "split   these words"))
print(bool(re.match(r"The", text)))
  • search — first match anywhere, or None
  • findall — every match, as a list
  • sub — replace
  • split — split on a pattern
  • match — only matches at the start

search returns None when nothing matches, so check before calling .group():

import re

result = re.search(r"\d+", "no digits here")
print(result)
print(result.group() if result else "no match")

Groups

Parentheses capture parts of a match:

import re

date_text = "Published 2026-03-15 by Ada"
m = re.search(r"(\d{4})-(\d{2})-(\d{2})", date_text)

print(m.group())
print(m.group(1))
print(m.groups())

Named groups are clearer than numbers:

import re

m = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})", "2026-03-15")
print(m.group("year"))
print(m.group("month"))
print(m.groupdict())

Greedy versus lazy

* and + are greedy — they take as much as possible. Add ? to make them lazy:

import re

html = "<b>bold</b> and <i>italic</i>"

print(re.findall(r"<.+>", html))
print(re.findall(r"<.+?>", html))

The greedy version swallows everything between the first < and the last >. This is one of the most common regex surprises.

Compiling

Reusing a pattern? Compile it once:

import re

pattern = re.compile(r"\b\w+@\w+\.\w+\b")
print(pattern.findall("a@b.com and c@d.org"))
print(bool(pattern.search("no emails")))

When not to use regex

Regex is powerful and easily overused:

  • Simple checks — "@" in text or text.startswith("http") beats a pattern.
  • HTML — use a parser (lesson 8.6).
  • Email validation — a truly correct pattern is monstrous. Check for @ and send a confirmation.

A regex you can't read six months later is a liability. If it needs a comment, consider a simpler approach.

Try it yourself

Checkpoint

  • Always write patterns as raw strings, r"...".
  • \d \w \s match classes; * + ? {n} set counts; ^ $ \b anchor position.
  • search finds one, findall finds all, sub replaces, match anchors to the start.
  • Parentheses capture groups; named groups (?P<name>...) read better.
  • * and + are greedy — add ? for lazy matching.
  • Don't use regex for HTML, or where a plain string method would do.

Questions about this lesson

Log in to ask a question.