LearnContact
Lesson 4728 min read

Regular Expressions (Regex)

Learn how to use Python's re module to search, match, and manipulate text using powerful pattern-matching rules.

Introduction

Imagine you need to check whether a string looks like a valid email address, pull every phone number out of a block of text, or replace every date in a document with a different format. Doing this with plain string methods like .find() or .split() quickly becomes messy and fragile.

Regular expressions (regex) solve this by giving you a compact mini-language for describing patterns in text. Python's built-in re module lets you search, extract, and replace text using these patterns. This lesson introduces regex from the ground up, using Python's re module.

What You Will Learn
  • What regular expressions are and why they are useful.
  • Basic pattern syntax: literals, ., \d, \w, \s, and quantifiers.
  • The difference between re.match(), re.search(), and re.findall().
  • How to replace text with re.sub().
  • How to extract parts of a match using groups.
  • Why regex patterns are usually written as raw strings.

What is Regex?

A regular expression is a sequence of characters that defines a search pattern. Instead of searching for one exact piece of text, you describe the shape of what you are looking for — "a group of digits," "a word followed by an @ symbol," and so on — and the regex engine finds every piece of text that matches that shape.

Pattern Matching

Describe the shape of text you want to find, not the exact text itself.

Powerful & Compact

A single short pattern can replace dozens of lines of manual string checking.

Language-Agnostic Idea

Regex syntax is nearly identical across Python, JavaScript, Java, and many other languages.

Everyday Uses

Form validation, log parsing, data cleaning, find-and-replace, and text extraction.

The re Module

Python's regex support lives in the built-in re module. You import it once, then use its functions to work with patterns.

import re

text = "The rain in Spain falls mainly on the plain."
result = re.findall(r"ain", text)
print(result)
Output
['ain', 'ain', 'ain', 'ain']

re.findall() scans the whole string and returns every non-overlapping match it finds, in order. This is the most commonly used regex function for simply collecting matches.

Raw Strings and Why We Use Them

Regex patterns frequently use the backslash character, for example \d for "a digit" or \w for "a word character." Normal Python strings also use backslashes for escape sequences like \n (newline) or \t (tab), which creates a conflict.

To avoid Python interpreting backslashes before the regex engine even sees them, regex patterns are almost always written as raw strings — strings prefixed with an r, like r"\d+". A raw string tells Python to treat backslashes as literal characters instead of escape codes.

import re

# Without a raw string, \d could be misread as an escape sequence.
pattern_normal = "\\d+"   # you would need to double the backslash
pattern_raw = r"\d+"     # much clearer with a raw string

print(pattern_normal == pattern_raw)
Output
True
Rule of Thumb
  • Always write regex patterns with an r prefix: r"pattern".
  • This avoids confusing backslash escape sequences.
  • It also makes the pattern easier to read and debug.

Basic Pattern Syntax

Most characters in a pattern match themselves literally. Special characters, called metacharacters, have a different meaning and let you match categories of characters instead of exact ones.

. (dot)

Matches any single character except a newline.

\d

Matches any digit, equivalent to [0-9].

\w

Matches any "word" character: letters, digits, and underscore.

\s

Matches any whitespace character: space, tab, newline.

\D \W \S

The uppercase versions match the opposite — non-digit, non-word, non-space.

import re

text = "Order #4521 shipped on 2026-01-15"

print(re.findall(r"\d", text))    # every single digit
print(re.findall(r"\d+", text))   # runs of digits grouped together
print(re.findall(r"\w+", text))   # word-like chunks
Output
['4', '5', '2', '1', '2', '0', '2', '6', '0', '1', '1', '5']
['4521', '2026', '01', '15']
['Order', '4521', 'shipped', 'on', '2026', '01', '15']

Quantifiers

Quantifiers control how many times the preceding character or group may repeat. Without them, every pattern would only ever match a single character at a time.

* (star)

Zero or more of the previous item.

+ (plus)

One or more of the previous item.

? (question mark)

Zero or one of the previous item — makes it optional.

{n,m}

Between n and m repetitions, e.g. {2,4}.

import re

print(re.findall(r"\d{3}", "12 345 6789"))     # exactly 3 digits
print(re.findall(r"\d{2,4}", "12 345 6789"))   # between 2 and 4 digits
print(re.findall(r"colou?r", "color and colour"))  # optional 'u'
Output
['345', '678']
['12', '345', '6789']
['color', 'colour']

Anchors: ^ and $

Anchors do not match characters themselves — they match a position in the string. ^ matches the start of the string, and $ matches the end.

import re

print(bool(re.match(r"^Hello", "Hello, world!")))   # starts with Hello
print(bool(re.search(r"world!$", "Hello, world!")))  # ends with world!
print(bool(re.search(r"^world!$", "Hello, world!"))) # neither start nor end alone matches whole string
Output
True
True
False

re.match() vs re.search() vs re.findall()

These three functions are the most commonly confused parts of the re module, because they sound similar but behave quite differently.

re.match()

Checks only the beginning of the string. Returns a Match object or None.

re.search()

Scans the whole string and returns the first match anywhere, or None.

re.findall()

Scans the whole string and returns a list of all matches, not just the first.

import re

text = "Call me at 555-1234 or 555-5678"

print(re.match(r"\d{3}-\d{4}", text))     # None, text doesn't start with digits
print(re.search(r"\d{3}-\d{4}", text))    # finds the first match only
print(re.findall(r"\d{3}-\d{4}", text))   # finds every match
Output
None
<re.Match object; span=(11, 19), match='555-1234'>
['555-1234', '555-5678']
Match Objects, Not Strings

re.match() and re.search() return a Match object (or None), not the matched text itself. Call .group() on the result to get the actual matched string.

import re

match = re.search(r"\d{3}-\d{4}", "Call 555-1234 now")
if match:
    print(match.group())
Output
555-1234

Replacing Text with re.sub()

re.sub(pattern, replacement, text) finds every match of the pattern and replaces it with the given replacement text, returning a brand-new string.

import re

text = "Contact: 555-1234 or 555-5678"
masked = re.sub(r"\d{3}-\d{4}", "XXX-XXXX", text)
print(masked)
Output
Contact: XXX-XXXX or XXX-XXXX

You can also limit how many replacements happen using the count argument, or use a function as the replacement for more advanced logic.

import re

text = "aaa bbb ccc"
print(re.sub(r"\w+", "word", text, count=2))
Output
word word ccc

Capturing Groups

Parentheses ( ) inside a pattern create a "group" — a portion of the match you can pull out separately. This is one of the most useful features of regex, since it lets you extract specific pieces of a matched string.

import re

text = "2026-01-15"
match = re.search(r"(\d{4})-(\d{2})-(\d{2})", text)

if match:
    print(match.group(0))  # the entire match
    print(match.group(1))  # first group: year
    print(match.group(2))  # second group: month
    print(match.group(3))  # third group: day
Output
2026-01-15
2026
01
15

You can also give groups names using (?P<name>...) syntax, which makes extracted data much easier to read.

import re

match = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", "2026-01-15")
print(match.group("year"))
print(match.group("month"))
Output
2026
01

Practical Example: Validating an Email and Extracting Numbers

Let's combine what we have learned into two small real-world examples: checking whether a string looks like a valid email address, and pulling all numbers out of a paragraph of text.

validate_email.py
import re

def is_valid_email(email):
    pattern = r"^[\w.]+@[\w-]+\.[a-zA-Z]{2,}$"
    return bool(re.match(pattern, email))

print(is_valid_email("student@example.com"))
print(is_valid_email("not-an-email"))
print(is_valid_email("bad@.com"))
Output
True
False
False
extract_numbers.py
import re

text = "There are 12 apples, 5 oranges, and 100 bananas in the basket."
numbers = re.findall(r"\d+", text)
numbers = [int(n) for n in numbers]
print(numbers)
print("Total items:", sum(numbers))
Output
[12, 5, 100]
Total items: 117
This is a Simplified Email Pattern

Real-world email validation patterns are far more complex due to the many valid email formats allowed by the spec. This pattern is a solid teaching example, not a production-ready validator.

Common Mistakes

Avoid These Mistakes
  • Forgetting the r prefix on regex patterns, causing confusing backslash behavior.
  • Using re.match() when you actually need re.search(), since match() only checks the start.
  • Forgetting that re.findall() returns strings (or tuples of groups), not Match objects.
  • Writing an overly greedy pattern that matches far more text than intended.
  • Not escaping special characters like . or ( when you want them matched literally — use \\. instead.

Best Practices

  • Always use raw strings (r"...") for regex patterns.
  • Use named groups (?P<name>...) for patterns with several captured pieces.
  • Compile a pattern with re.compile() if you will reuse it many times, for a small performance gain.
  • Test tricky patterns against several example strings, including edge cases.
  • Keep patterns as simple as possible — a long, unreadable regex is hard to maintain.

Frequently Asked Questions

What is the difference between re.search() and re.findall()?

re.search() returns only the first match as a Match object (or None). re.findall() returns a list of every match found in the string.

Why do regex patterns use raw strings?

Raw strings prevent Python from interpreting backslashes as escape sequences before the regex engine processes the pattern, avoiding confusing bugs.

Can regex match multiple lines of text?

Yes, by default . does not match newlines, but you can pass the re.DOTALL flag, or use the re.MULTILINE flag to make ^ and $ match the start/end of each line.

Is regex the best way to validate all input, like emails?

Regex is great for basic format checks, but for critical validation (like confirming an email is real) you typically also need to send a confirmation message, since regex only checks the shape of the text.

What does group(0) mean?

group(0), or just group(), returns the entire matched text. group(1), group(2), and so on return the text captured by each parenthesized group in order.

Key Takeaways

  • The re module provides Python's regular expression support.
  • Regex patterns describe the shape of text using literals and metacharacters like \d, \w, and \s.
  • Quantifiers (*, +, ?, {n,m}) control how many times a pattern repeats.
  • re.match() checks the start, re.search() finds the first match anywhere, and re.findall() finds all matches.
  • re.sub() replaces matched text, and parentheses create groups for extracting specific pieces of a match.
  • Regex patterns should almost always be written as raw strings.

Summary

Regular expressions give you a powerful, compact way to search, validate, and transform text that would otherwise require many lines of manual string handling. Python's re module puts this power just an import away.

In this lesson, you learned regex pattern syntax, the differences between match(), search(), and findall(), how to replace text with sub(), and how to extract data using capturing groups.

Lesson Completed
  • You understand core regex pattern syntax and quantifiers.
  • You can search, match, and replace text using the re module.
  • You can extract specific pieces of matched text using groups.
  • You are ready to explore working with dates and times in Python.
Next Lesson →

Date & Time