Python RegEx: re Module Patterns with Examples - Python with AI tutorial chapter 29
Python

Python RegEx: re Module Patterns with Examples

Python with AI Tutorial · Chapter 29 of 48

Python regex (regular expressions) is pattern matching for text, provided by the built-in re module. Instead of checking for one fixed word, you describe a shape: “three letters, a dash, four digits” or “anything between angle brackets”. With search, findall, sub and fullmatch you can find, extract, replace and validate text in a few lines.

Regex has a reputation for looking like noise, and that reputation is partly earned. The trick is to learn about a dozen metacharacters, always write patterns as raw strings (r"...") so backslashes survive, and test each pattern on a few real lines before trusting it. This chapter follows that approach with invoices, emails, GST numbers and log files.

Finding the first match with re.search

re.search scans the whole string and returns a Match object for the first hit, or None if nothing matches. The match object tells you what was found and where.

import re

text = "Invoice INV-2026-0451 was issued on 31-03-2026 for Rs 12,500."

m = re.search(r"INV-\d{4}-\d{4}", text)
print(m)
print(m.group(), m.start(), m.end())
print(bool(re.search(r"paid", text)))
Output:
<re.Match object; span=(8, 21), match='INV-2026-0451'>
INV-2026-0451 8 21
False

Here \d means any digit and {4} means exactly four of them. Because None is falsy, if re.search(...) is the idiomatic way to ask “does this text contain the pattern?”

Metacharacter Matches Example
. Any character except newline a.c matches abc, a-c
\d / \D Digit / non-digit \d{2} matches 31
\w / \W Letter, digit or underscore / anything else \w+ matches INV_2026
\s / \S Whitespace / non-whitespace \s+ matches spaces and tabs
[abc] / [^abc] One character in the set / not in the set [A-Z]{5} matches AAPFU
^ / $ Start / end of string (or line with MULTILINE) ^ERROR
* / + / ? 0 or more / 1 or more / 0 or 1 colou?r matches color, colour
{n} / {n,m} Exactly n / between n and m \d{4,6}
( ) Capture group (\d{2})-(\d{2})
(?P<name> ) Named capture group (?P<year>\d{4})
a|b Either a or b INR|Rs
\b Word boundary \bQ1\b

Extracting every match with re.findall

re.findall returns a list of every non-overlapping match. It is the fastest way to pull all numbers, codes or emails out of a block of text, but the first attempt below shows why you must look at the result before using it.

import re

report = "Q1 sales 420000, Q2 sales 465000, refunds 12500 and 3200."

print(re.findall(r"\d+", report))
print(re.findall(r"\b\d{4,}\b", report))

amounts = [int(n) for n in re.findall(r"\b\d{4,}\b", report)]
print("Total mentioned:", sum(amounts))
Output:
['1', '420000', '2', '465000', '12500', '3200']
['420000', '465000', '12500', '3200']
Total mentioned: 901200

The plain \d+ also grabbed the 1 and 2 from Q1 and Q2. Adding a minimum length and word boundaries fixed it. Expect two or three rounds like this with any new pattern.

Capture groups: pulling out the parts

Parentheses capture pieces of the match so you can use them separately. Numbered groups start at 1 (group(0) is the whole match); named groups with (?P<name>...) make the code self-documenting.

import re

line = "Priya Sharma <priya.sharma@neotech.in> joined 2019-07-01"

m = re.search(r"<([\w.]+)@([\w.]+)>", line)
print(m.group(0))
print("User:", m.group(1), "Domain:", m.group(2))

d = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", line)
print(d.group("year"), d["month"])
print(d.groupdict())
Output:
<priya.sharma@neotech.in>
User: priya.sharma Domain: neotech.in
2019 07
{'year': '2019', 'month': '07', 'day': '01'}

Replacing with re.sub

re.sub(pattern, replacement, text) replaces every match. The replacement can refer back to captured groups with \1, \2 and so on, which makes reformatting dates or masking sensitive digits a one-liner.

import re

messy = "Contact:  +91  80770 90260   or   080-2345-6789 "
print(re.sub(r"\s+", " ", messy).strip())

card = "Paid with card 4111 1111 1111 1234"
print(re.sub(r"\d{4}(?= \d{4})", "****", card))

print(re.sub(r"(\d{2})-(\d{2})-(\d{4})", r"\3-\2-\1", "Due 31-03-2026, paid 12-05-2026"))
Output:
Contact: +91 80770 90260 or 080-2345-6789
Paid with card **** **** **** 1234
Due 2026-03-31, paid 2026-05-12

The card example uses a lookahead, (?= \d{4}): it requires a following group of four digits to be present but does not consume it, so only the first three groups are masked and the last four digits stay visible.

Validating with re.fullmatch

search is happy if the pattern appears anywhere. For validation you want fullmatch, which succeeds only when the entire string fits. Compiling a pattern once with re.compile is tidier and faster when you check thousands of rows.

import re

GSTIN = re.compile(r"\d{2}[A-Z]{5}\d{4}[A-Z][1-9A-Z]Z[0-9A-Z]")
PAN = re.compile(r"[A-Z]{5}\d{4}[A-Z]")
EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")

for g in ["27AAPFU0939F1ZV", "27aapfu0939f1zv", "27AAPFU0939F1Z"]:
    print(g, "valid" if GSTIN.fullmatch(g) else "invalid")

print(bool(PAN.fullmatch("AAPFU0939F")))
print(bool(EMAIL.fullmatch("accounts@neotech.in")), bool(EMAIL.fullmatch("accounts@neotech")))
Output:
27AAPFU0939F1ZV valid
27aapfu0939f1zv invalid
27AAPFU0939F1Z invalid
True
True False
Common mistake: forgetting the r prefix. In a normal string "\b" is a backspace character and "\d" triggers a SyntaxWarning on Python 3.12. Write every pattern as r"..." and the backslashes reach the regex engine unchanged.

Splitting text and using flags

re.split breaks a string on a pattern rather than a single character, which handles inconsistent delimiters in one pass. Flags change how matching works: re.IGNORECASE for case-insensitive, re.MULTILINE so ^ and $ work per line.

import re

raw = "Laptop;Mouse, Keyboard |Monitor"
print(re.split(r"\s*[;,|]\s*", raw))

log = """ERROR disk full
info backup done
Error timeout on node 3"""

errors = re.findall(r"^error.*$", log, flags=re.IGNORECASE | re.MULTILINE)
print(errors)
print(len(errors), "error lines")
Output:
['Laptop', 'Mouse', 'Keyboard', 'Monitor']
['ERROR disk full', 'Error timeout on node 3']
2 error lines

Greedy versus lazy matching

Quantifiers are greedy by default: .* grabs as much as it can while still letting the pattern succeed. Adding ? makes it lazy, stopping at the first opportunity. This is the difference between one giant match and a clean list.

import re

html = "<b>Total</b>: <b>4,599</b>"

print(re.findall(r"<b>.*</b>", html))
print(re.findall(r"<b>(.*?)</b>", html))

for m in re.finditer(r"<b>(.*?)</b>", html):
    print(m.group(1), "at", m.span())
Output:
['<b>Total</b>: <b>4,599</b>']
['Total', '4,599']
Total at (0, 12)
4,599 at (14, 26)

finditer yields match objects one at a time, so you get positions and groups together. It is also memory-friendly on very large files because nothing is collected into a list.

Try it with AI

Ask for a validated pattern with a plain-English breakdown.

Write a Python regex that validates Indian GST numbers (GSTIN): 2-digit state code, 10-character PAN, 1 entity code, the letter Z, then a check character. Explain each part of the pattern on its own line, then show a test loop over 5 valid and 5 invalid examples using re.fullmatch.
Try it with AI

Turn a messy text export into structured rows.

I have bank statement lines like "05/09/2026 UPI/PRIYA SHARMA/RENT SEPT 25,000.00 Dr" and "04/09/2026 NEFT/ACME LTD/INV-104 1,25,000.00 Cr". Write a Python function using named regex groups that extracts date, channel, counterparty, description, amount (as float, handling Indian comma grouping) and Dr/Cr, and returns a list of dicts. Include 3 test lines and print the result with json.dumps(indent=2).

Common mistakes

  • Writing patterns without the r prefix, so \b and \d are mangled before the regex engine sees them.
  • Using re.match when you mean re.search; match only checks the start of the string.
  • Validating with search instead of fullmatch, which accepts “27AAPFU0939F1ZV extra text”.
  • Forgetting that . does not match a newline unless you pass re.DOTALL.
  • Reaching for regex to parse real HTML or JSON. Use a parser for those; regex is for flat text.

Exercise

Given text = "Orders: ORD-1001 (2 items), ORD-1002 (5 items), ORD-1017 (1 item)", use one regex with two capture groups to build a dictionary mapping each order id to its item count as an integer.

Show answer
import re

text = "Orders: ORD-1001 (2 items), ORD-1002 (5 items), ORD-1017 (1 item)"
pairs = re.findall(r"(ORD-\d+) \((\d+) items?\)", text)
orders = {order_id: int(count) for order_id, count in pairs}

print(orders)   # {'ORD-1001': 2, 'ORD-1002': 5, 'ORD-1017': 1}

Related chapters

FAQ

What is the difference between re.match, re.search and re.fullmatch?

match checks only at the start of the string, search finds the pattern anywhere, and fullmatch requires the entire string to fit the pattern. Use fullmatch for validation.

Why do I need r before a regex string in Python?

The r makes a raw string, so backslashes are passed through unchanged. Without it, \b becomes a backspace and \d raises a SyntaxWarning in Python 3.12.

How do I make a Python regex case-insensitive?

Pass flags=re.IGNORECASE (or re.I) to the function, or put (?i) at the start of the pattern. Combine flags with the | operator.

Working with spreadsheets too? Ready-made Excel, Google Sheets and Power BI templates are at NextGenTemplates.com.

Chapter 29 of 48 · Python with AI: all 48 chapters

PK
Meet PK, the founder of NeotechNavigators.com! With over 15 years of experience in Data Visualization, Excel Automation, and dashboard creation. PK is a Microsoft Certified Professional who has a passion for all things in Excel. PK loves to explore new and innovative ways to use Excel and is always eager to share his knowledge with others. With an eye for detail and a commitment to excellence, PK has become a go-to expert in the world of Excel. Whether you're looking to create stunning visualizations or streamline your workflow with automation, PK has the skills and expertise to help you succeed. Join the many satisfied clients who have benefited from PK's services and see how he can take your data analysis skills to the next level!
https://neotechnavigators.com