0%

55- Regular Expressions (RegEx)

Powerful pattern matching for strings. Search, extract, replace, and validate text with concise patterns. A Swiss Army knife for text processing.

You have searched for substrings with in. You have replaced text with .replace(). But what if you need more? Find all email addresses in a document. Validate if a phone number has the correct format. Extract dates from messy text. Split a string on multiple delimiters at once. Regular expressions (regex) are the answer. A regular expression is a sequence of characters that defines a search pattern. It is a small language embedded inside Python. With regex, you can perform complex text matching, extraction, and replacement in just a few lines of code. This lesson introduces the re module and the most common regex patterns. You will learn to search, find all matches, replace text, and validate formats. Regex can seem cryptic at first, but with practice, it becomes an indispensable tool.

🕯️ Magic Note

Regular expressions date back to the 1950s and the work of mathematician Stephen Kleene. They were introduced to Unix in the 1970s and have since become a standard feature in almost every programming language. Python’s re module implements Perl-compatible regular expressions (PCRE).

Raw Strings for Regex
Always use raw strings for regex patterns. Raw strings treat backslashes as literal characters, not escape sequences.

Python

# Without raw string (backslashes need escaping)

pattern = “\\d+\\.\\d{2}” # Hard to read

# With raw string (recommended)

pattern = r”\d+\.\d{2}” # Clean and readable

💡 Always prefix your regex patterns with r. It saves you from endless backslash escaping and makes patterns readable. Example: r”\d+” instead of “\\d+”.
The re Module Basics
The re module provides functions for working with regular expressions.

Python

import re

text = “The price is $19.99 and $29.99”

# search(): find first match

match = re.search(r”\$\d+\.\d{2}”, text)

if match:

print(f”First match: {match.group()}”)

# findall(): find all matches (returns list)

all_prices = re.findall(r”\$\d+\.\d{2}”, text)

print(f”All prices: {all_prices}”)

# finditer(): find all matches (returns iterator of match objects)

for match in re.finditer(r”\$\d+\.\d{2}”, text):

print(f”Found: {match.group()} at position {match.start()}-{match.end()}”)

# sub(): replace all matches

redacted = re.sub(r”\$\d+\.\d{2}”, “$XX.XX”, text)

print(f”Redacted: {redacted}”)

🕯️ Magic Note

The re.match() function checks for a match only at the beginning of the string. re.search() scans the entire string. re.fullmatch() requires the entire string to match. Choose the right function for your use case.

Regex Pattern Basics: Literals and Metacharacters
Most characters match themselves. But some have special meanings.
PatternMatches
“hello”The literal string “hello”
“.”Any single character (except newline)
“^”Start of string
“$”End of string
“*”0 or more repetitions of the previous pattern
“+”1 or more repetitions of the previous pattern
“?”0 or 1 repetition of the previous pattern
“{n}”Exactly n repetitions
“{n,m}”Between n and m repetitions
“|”OR (matches either pattern)
“[]”Character class (matches any character inside)
“[^]”Negated character class
“\d”Any digit (0-9)
“\D”Any non-digit
“\w”Any word character (a-z, A-Z, 0-9, _)
“\W”Any non-word character
“\s”Any whitespace (space, tab, newline)
“\S”Any non-whitespace
“\b”Word boundary
“\B”Non-word boundary

Python

import re

text = “The year is 2025, and the temperature is 25.5°C”

# \d matches digits

print(re.findall(r”\d”, text)) # [‘2’, ‘0’, ‘2’, ‘5’, ‘2’, ‘5’, ‘5’]

print(re.findall(r”\d+”, text)) # [‘2025′, ’25’, ‘5’]

# \w matches word characters

print(re.findall(r”\w+”, text)) # [‘The’, ‘year’, ‘is’, ‘2025’, ‘and’, ‘the’, ‘temperature’, ‘is’, ’25’, ‘5’, ‘C’]

# \s matches whitespace

print(re.findall(r”\s+”, text)) # [‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘, ‘ ‘]

# [] character class

print(re.findall(r”[aeiou]”, text)) # [‘e’, ‘e’, ‘i’, ‘a’, ‘e’, ‘e’, ‘e’, ‘i’, ‘u’]

print(re.findall(r”[0-9]+”, text)) # [‘2025′, ’25’, ‘5’]

# Anchors: ^ (start) and $ (end)

print(re.findall(r”^\w+”, text)) # [‘The’] (word at start)

print(re.findall(r”\w+$”, text)) # [‘C’] (word at end)

Quantifiers: Repeating Patterns
Quantifiers specify how many times a pattern should repeat.

Python

import re

text = “Colors: red, green, blue, darkred, lightblue”

# {n} – exactly n times

print(re.findall(r”\w{4}”, text)) # [‘blue’, ‘dark’, ‘blue’] (4 letter words)

# {n,m} – between n and m times

print(re.findall(r”\w{3,5}”, text)) # [‘red’, ‘gree’, ‘blue’, ‘dark’, ‘light’, ‘blue’]

# * – 0 or more (greedy)

print(re.findall(r”colou*r”, “color colour colur colr”)) # [‘color’, ‘colour’, ‘colur’, ‘colr’]

# + – 1 or more

print(re.findall(r”\d+”, “a1 b22 c333”)) # [‘1′, ’22’, ‘333’]

# ? – 0 or 1

print(re.findall(r”https?://”, “http:// https:// ftp://”)) # [‘http://’, ‘https://’]

🕯️ Magic Note

Quantifiers are greedy by default. They match as much as possible. Add ? after a quantifier to make it non-greedy (lazy): .*? matches the shortest possible match instead of the longest.

Groups: Capturing and Extracting
Parentheses create groups. You can extract specific parts of a match.

Python

import re

text = “Name: Ali, Age: 25, City: Tehran”

# Single group (extract one part)

match = re.search(r”Age: (\d+)”, text)

if match:

print(f”Age group 1: {match.group(1)}”) # First group

print(f”Full match: {match.group(0)}”) # Entire match

# Multiple groups

match = re.search(r”Name: (\w+), Age: (\d+), City: (\w+)”, text)

if match:

name, age, city = match.groups()

print(f”Name: {name}, Age: {age}, City: {city}”)

# findall with groups returns tuples

data = “Ali:25, Sara:30, Reza:28”

pairs = re.findall(r”(\w+):(\d+)”, data)

print(pairs) # [(‘Ali’, ’25’), (‘Sara’, ’30’), (‘Reza’, ’28’)]

# finditer with groups

for name, age in re.finditer(r”(\w+):(\d+)”, data):

print(f”name: {name.group(1)}, age: {age.group(2)}”)

💡 Use groups to extract specific information from matches. For example, extract usernames and domains from email addresses with r”(\w+)@(\w+\.\w+)”.
Named Groups
Named groups make your regex more readable. Use (?P<name>pattern).

Python

import re

text = “Order: #12345, Date: 2025-05-10”

pattern = r”Order: #(?P<order_id>\d+), Date: (?P<date>\d{4}-\d{2}-\d{2})”

match = re.search(pattern, text)

if match:

print(f”Order ID: {match.group(‘order_id’)}”)

print(f”Date: {match.group(‘date’)}”)

print(f”All named groups: {match.groupdict()}”)

# {‘order_id’: ‘12345’, ‘date’: ‘2025-05-10’}

🕯️ Magic Note

Named groups are especially useful when your regex has many groups. Instead of remembering positions, you use meaningful names. This makes your code self-documenting.

Character Classes and Sets
Character classes match specific sets of characters.

Python

import re

text = “abc123 DEF!@#”

# Predefined classes

print(re.findall(r”\d”, text)) # [‘1’, ‘2’, ‘3’] (digits)

print(re.findall(r”\w”, text)) # [‘a’,’b’,’c’,’1′,’2′,’3′,’D’,’E’,’F’] (word chars)

print(re.findall(r”\s”, text)) # [‘ ‘, ‘ ‘] (whitespace)

# Custom classes

print(re.findall(r”[aeiou]”, text)) # [‘a’] (vowels)

print(re.findall(r”[A-Z]”, text)) # [‘D’, ‘E’, ‘F’] (uppercase)

print(re.findall(r”[a-z]”, text)) # [‘a’, ‘b’, ‘c’] (lowercase)

# Negated classes [^]

print(re.findall(r”[^a-zA-Z]”, text)) # [‘1′,’2′,’3′,’ ‘,’!’,’@’,’#’]

# Character ranges

print(re.findall(r”[0-9a-f]”, “12af 89gh”)) # [‘1′,’2′,’a’,’f’,’8′,’9′]

Escaping Special Characters
To match a literal metacharacter (like ., *, +, ?, ^, $, [, ], (, ), {, }, |), escape it with a backslash.

Python

import re

text = “What is 2 + 2? Answer: 4.”

# Match literal plus sign (needs escaping)

print(re.findall(r”\+”, text)) # [‘+’]

# Match literal dot (needs escaping)

print(re.findall(r”\.”, text)) # [‘.’]

# Match literal parentheses

print(re.findall(r”\(\)”, “( )”)) # [‘()’]

# Match literal asterisk

print(re.findall(r”\*”, “*”)) # [‘*’]

Compiling Patterns for Performance
If you use the same pattern many times, compile it with re.compile() for better performance.

Python

import re

# Compile once

email_pattern = re.compile(r”[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}”)

# Reuse many times (faster)

if email_pattern.match(“user@example.com”):

print(“Valid email 1”)

if email_pattern.match(“another@test.org”):

print(“Valid email 2”)

# Compiled patterns have the same methods

all_emails = email_pattern.findall(“Contact: user@example.com and admin@test.org”)

print(all_emails) # [‘user@example.com’, ‘admin@test.org’]

💡 Compile patterns when you use them frequently in loops or many times. The performance difference is significant. For one-off matches, inline functions like re.search() are fine.
Common Patterns
Here are practical regex patterns you will encounter often.

Python

import re

# Email addresses

email = r”[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}”

# Phone numbers (US format)

phone = r”\d{3}[-.]?\d{3}[-.]?\d{4}”

# Dates (YYYY-MM-DD)

date = r”\d{4}-\d{2}-\d{2}”

# URL (simple version)

url = r”https?://[a-zA-Z0-9./_-]+”

# IP address (simplified)

ip = r”\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}”

# Hex color code

hex_color = r”#[0-9a-fA-F]{6}”

# US ZIP code

zip_code = r”\d{5}(?:-\d{4})?”

Flags: Modify Regex Behavior
Flags change how the regex engine interprets the pattern.

Python

import re

text = “Hello World\nHELLO WORLD”

# re.IGNORECASE (or re.I) – case insensitive

print(re.findall(r”hello”, text, re.IGNORECASE)) # [‘Hello’, ‘HELLO’]

# re.MULTILINE (or re.M) – ^ and $ match line starts/ends

print(re.findall(r”^HELLO”, text, re.MULTILINE)) # [‘HELLO’] (second line)

# re.DOTALL (or re.S) – dot matches newline as well

print(re.findall(r”Hello.*World”, text, re.DOTALL)) # [‘Hello World\nHELLO WORLD’]

# re.VERBOSE (or re.X) – allow comments and whitespace in pattern

pattern = re.compile(r”””

\d{4} # Year

– # Separator

\d{2} # Month

– # Separator

\d{2} # Day

“””, re.VERBOSE)

print(pattern.findall(“2025-05-10”)) # [‘2025-05-10’]

🕯️ Magic Note

The re.VERBOSE flag makes complex regex patterns readable. You can add comments and whitespace. Use it for patterns that are hard to understand at a glance.

Lookahead and Lookbehind (Advanced)
Zero-width assertions that check what comes before or after without including it in the match.

Python

import re

text = “I have 10 apples and 20 oranges”

# Positive lookahead (?=…) – match if followed by

print(re.findall(r”\d+(?= apples)”, text)) # [’10’] (numbers before “apples”)

# Negative lookahead (?!…) – match if not followed by

print(re.findall(r”\d+(?! apples)”, text)) # [’20’] (numbers not before “apples”)

# Positive lookbehind (?<=…) – match if preceded by

print(re.findall(r”(?<=apples )\d+”, text)) # [] (numbers after “apples”)

print(re.findall(r”(?<=oranges )\d+”, text)) # []? wait no number after oranges

# Negative lookbehind (?<!…) – match if not preceded by

print(re.findall(r”(?<!apples )\d+”, text)) # [’10’, ’20’]? actually need careful

Practical Example: Log Parser
Parse a log file to extract important information.

Python

import re

log_line = ‘2025-05-10 14:30:45,123 ERROR [main] Connection failed: timeout (user=ali, ip=192.168.1.1)’

pattern = re.compile(

r”(?P<date>\d{4}-\d{2}-\d{2})”

r”\s+”

r”(?P<time>\d{2}:\d{2}:\d{2},\d{3})”

r”\s+”

r”(?P<level>\w+)”

r”\s+”

r”\[(?P<component>\w+)\]”

r”\s+”

r”(?P<message>.*?)\s*\(user=(?P<user>\w+),\s*ip=(?P<ip>[\d\.]+)\)”

)

match = pattern.match(log_line)

if match:

print(f”Date: {match.group(‘date’)}”)

print(f”Time: {match.group(‘time’)}”)

print(f”Level: {match.group(‘level’)}”)

print(f”Component: {match.group(‘component’)}”)

print(f”Message: {match.group(‘message’)}”)

print(f”User: {match.group(‘user’)}”)

print(f”IP: {match.group(‘ip’)}”)

Common Mistakes with Regex
  • Forgetting to use raw strings (r”…”) causing unintended backslash escapes
  • Using greedy quantifiers when non-greedy would be appropriate
  • Assuming re.match() searches the entire string (it only checks the start)
  • Not escaping special characters that should be literal
  • Making regex patterns too complex (break them into smaller steps)
  • Using regex for simple string operations where .find() or .replace() would suffice
Check Your Understanding
  • Write a regex to find all email addresses in a string.
  • What is the difference between re.search() and re.findall()?
  • How do you extract the domain part from an email address using groups?
  • What does the pattern r”\d{3}-\d{2}-\d{4}” match?
  • What is the purpose of re.compile()?
  • Write a regex that validates a US phone number format: (123) 456-7890 or 123-456-7890

⚡ Whisper

Regular expressions are a language within a language. At first, they look like line noise. \d+\.\d{2} seems cryptic. But each symbol has meaning. \d is a digit. + means one or more. \. is a literal dot. \d{2} means two digits. Once you learn the symbols, you see patterns everywhere. Emails become \w+@\w+\.\w+. Dates become \d{4}-\d{2}-\d{2}. Phone numbers become patterns of digits and dashes. Regex is not magic. It is a skill. Start simple. Practice. Use online regex testers (regex101.com). Break complex patterns into pieces. Add comments with re.VERBOSE. And remember: regex is powerful, but power invites complexity. For simple string operations, .find() and .replace() are often enough. Use regex when you need flexibility. Use regex when patterns vary. Use regex when you need to extract meaning from chaos. Then the chaos becomes order.

Related posts