Regular Expressions (the re module)
Reviewed & published by Brayan K
A regular expression is a tiny pattern language for describing text — "any digit", "an email", "a word at the end of a line". Python's re module turns those patterns into powerful search-and-replace tools.
Learn Regular Expressions (the re module) in our free Python course — a beginner-friendly interactive lesson with runnable examples, a practice exercise and…
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
Once you can read and write regex, validating phone numbers, pulling dates out of log files, or cleaning messy data becomes a few lines instead of a hundred.
What You'll Learn in This Lesson
1 What Is a Regular Expression?
A regular expression (regex) is a string that describes a pattern of text. Instead of asking "does this string contain the exact word cat?", regex lets you ask "does this string contain three digits, then a dash, then four digits?".
- Validating that a user typed a real email address
- Extracting all the dates or prices from a block of text
- Finding and replacing patterns (e.g. masking credit-card numbers)
- Splitting a log line into fields without fragile string slicing
Everything starts with importing the module — it ships with Python, nothing to install:
That pattern \d {3} -\d {3} -\d {4} reads as: "three digits, a dash, three digits, a dash, four digits". You just described a US phone number in 17 characters.
2 Raw Strings — The First Rule of Regex
Regex patterns are full of backslashes ( \d , \w , \b ). But in a normal Python string, \ is the escape character — "\n" means newline, "\t" means tab. That clash causes endless confusion.
3 The Five Functions You'll Actually Use
The re module has many functions, but 95% of real work uses just five:
Function
What it does
🧱 The Building Blocks of a Pattern
Character classes — shortcuts for common groups
4 Greedy vs Lazy — The Classic Gotcha
By default quantifiers are greedy : they grab as much as possible. Add a ? to make them lazy — grab as little as possible. This trips up everyone the first time:
5 Capture Groups — Pulling Out the Pieces
Parentheses (...) create a capture group . After a match you can pull out each group individually — perfect for parsing structured text.
Named groups ( (?P<name>...) ) make patterns self-documenting and let you reference fields by name instead of counting parentheses.
📖 Worked Example: Parsing a Log File
This is what regex is genuinely great at: turning lines of text that follow a shape into structured data. Read it once before you write anything — every piece of the pattern is one of the building blocks you have just met.
Two new-but-small things appear here. re.VERBOSE lets you spread a pattern across several lines and comment each part, which is the difference between a maintainable pattern and write-only line noise. And re.compile builds the pattern once instead of on every loop iteration.
🎯 Your Turn: Invoice IDs and Due Dates
Three blanks, one concept each: a character class, a group name, and the method that hands back every named group at once. Fill them in and run it — the expected output is at the bottom of the code so you can check yourself.
1) \d — so the pattern is r"INV-\d{4}-\d{4}" . Note the r prefix: without it Python would read \d as an escape sequence rather than passing it to the regex engine.
2) month — the middle piece of a YYYY-MM-DD date.
3) groupdict — m.groupdict() . Use m.groups() instead and you get an unnamed tuple, ('2024', '03', '01') .
If Invoice IDs comes back empty, check you kept the literal INV- in front — the digit class alone matches nothing here because it needs the surrounding text to anchor it.
Real-World Example: Cleaning and Validating a Sign-Up Form
Here's the kind of validation code that runs behind every registration form. Notice how each rule is a small, readable pattern:
The "perfect" email regex is famously gigantic. In real apps, a simple pattern like the one above plus sending a confirmation email is the practical standard. Don't try to validate every RFC edge case with regex alone.
⚙️ re.compile and Useful Flags
If you reuse a pattern, compile it once into a Pattern object . It's faster and reads better in loops:
Common flags: re.IGNORECASE (case-insensitive), re.MULTILINE ( ^ / $ match each line), re.DOTALL ( . also matches newlines).
🏆 Mini-Challenge: Clean Up a Social Post
Outline only — you write all four patterns. Take one messy social post and pull out its hashtags, its email addresses and its launch date, then produce a copy with the emails hidden. This is real work: it is exactly what a moderation or analytics pipeline does to every incoming message.
One deliberate trap: [email protected] has two dots after the @. Make sure your pattern still catches it.
The hashtag pattern captures (\w+) in a group, which is why findall returns the words without the # — when a pattern has exactly one group, findall gives you that group instead of the whole match.
🧩 Reorder Challenge
These lines extract all hashtags from a tweet and print them lowercased — but they're scrambled. Put them in the correct order:
Import first, define the data, run findall to collect the captures, then loop. The capture group (\w+) means findall returns just the word, without the # .
🧠 Quick Recall — Predict the Output
Read each snippet and predict what prints before revealing the answer.
['1', '22', '333'] — \d+ grabs each run of one-or-more digits as a separate match.
hello_big_world — \s+ matches each run of whitespace (even multiple spaces) and replaces the whole run with a single underscore.
False — re.match only checks the START of the string, and "the cat" starts with "the". Use re.search to find it anywhere.
❓ Frequently Asked Questions
You can now describe text patterns like a pro!
You've learned the five core re functions, character classes, quantifiers, greedy vs lazy matching, and capture groups. Regex is a skill you sharpen by using it — keep a cheat sheet handy and test patterns on small strings first.
🚀 Up next: Dates & Times — work with the datetime module to parse, format, and do arithmetic with dates.
Practice quiz
What is the difference between re.match and re.search?
- match scans the whole string; search only the start
- They are identical
- match checks only the START of the string; search scans the whole string
- search only works on numbers
Answer: match checks only the START of the string; search scans the whole string. re.match only matches at the very start of the string, while re.search scans the whole string for the first match anywhere.
Why write regex patterns as raw strings like r'\d+'?
- So backslashes are treated literally and reach re unchanged
- They run faster
- Raw strings allow Unicode
- It is required by Python syntax
Answer: So backslashes are treated literally and reach re unchanged. Regex uses many backslashes; a raw string (r'...') tells Python to treat them literally instead of as escape sequences like .
What does re.findall(r'\d+', 'a1b22c333') return?
- 1
- 2
- 3
\d+ grabs each run of one-or-more digits as a separate match, giving the list of strings ['1', '22', '333'].
What does bool(re.match(r'cat', 'the cat')) evaluate to?
- True
- False
- None
- It raises an error
Answer: False. re.match only checks the START of the string, and 'the cat' starts with 'the', so the result is False. Use re.search to find it anywhere.
By default, quantifiers like .* are:
- Greedy (match as much as possible)
- Lazy (match as little as possible)
- Disabled
- Case-insensitive
Answer: Greedy (match as much as possible). Quantifiers are greedy by default, grabbing as much as possible. Add ? (as in .*?) to make them lazy.
What does the lazy pattern r'<.*?>' match in '<b>bold</b>'?
- The whole string at once
- Only the first letter
- Each tag separately: '<b>', '</b>'
- Nothing
Answer: Each tag separately: '<b>', '</b>'. The lazy .*? stops at the first > it can, so it matches each tag individually rather than spanning from the first < to the last >.
How do you create a capture group in a regex?
Parentheses (...) create a capture group; you then retrieve each piece with .group(1), .group(2), etc., or via re.findall.
What does re.sub(r'\s+', '_', 'hello big world') return?
- 'hello___big___world'
- 'hello big world'
- '_hello_big_world_'
- 'hello_big_world'
Answer: 'hello_big_world'. \s+ matches each run of whitespace (even multiple spaces) and replaces the whole run with one underscore, giving 'hello_big_world'.
When re.search finds no match, what does it return?
- An empty string
- None
- An empty Match object
- It raises ValueError
Answer: None. re.search (and re.match) return None when nothing matches, so always check before calling .group() to avoid an AttributeError.
What is the benefit of re.compile(pattern)?
- It validates the pattern only
- It makes the regex case-insensitive
- It builds a reusable Pattern object that is faster for repeated use
- It converts the pattern to a string
Answer: It builds a reusable Pattern object that is faster for repeated use. re.compile turns a pattern into a reusable Pattern object — faster and clearer when you apply the same pattern many times.
Continue this course
- Previous: Checkpoint: Python Essentials
- Next: Dates & Times (datetime)