What’s one thing you learned? What’s still confusing?
NumPy & Pandas: The Data Science Toolkit
NumPy arrays, vectorization, broadcasting, and Pandas DataFrames.
Pandas Advanced: merge, pivot, apply, Time Series
Join DataFrames, apply functions, reshape with pivot_table, and build time series features.
Data Visualization with Matplotlib
Line plots, scatter plots, bar charts, histograms, and multi-panel figures.
Interactive Labs for This Track
Loop Visualizer
You're a factory robot repeating the same task on an assembly line — watch how loops automate repetitive work
List Slicing
You have a playlist of 50 songs — grab just tracks 10 through 20 with a single slice expression
Sorting Algorithms
You're organizing a library of 10,000 books — which sorting method is fastest?
Ask questions, share insights
Python's built-in string methods are great for simple cases, but they hit a wall quickly:
# Simple case: string methods work fine
text = "Hello, World!"
print("World" in text) # True
print(text.replace("World", "Python")) # "Hello, Python!"
print(text.startswith("Hello")) # TrueBut what about these tasks?
# Task 1: Find all phone numbers in text (any format)
text = "Call me at 415-555-1234 or (555) 867-5309 or 555.123.4567"
# str.find()? You'd need multiple calls for each format...
# Task 2: Validate an email address
email = "user@example.com"
# How many string checks would you need? @, dot, no spaces, valid chars...
# Task 3: Extract all prices from a product listing
listing = "Widget A: $12.99, Widget B: $1,234.56, Widget C: $0.99"
# str.split("$")? What about the comma in 1,234.56?
# Task 4: Replace multiple date formats with a single standard
log = "2024-01-15 error, 01/15/2024 warning, Jan 15 2024 info"
# You'd need a separate replace() for every format...import re
# Task 1: Find ALL phone numbers, regardless of format
text = "Call me at 415-555-1234 or (555) 867-5309 or 555.123.4567"
phones = re.findall(r'\(?\d{3}\)?[\s.-]?\d{3}[\s.-]?\d{4}', text)
print(phones)
# ['415-555-1234', '(555) 867-5309', '555.123.4567']
# Task 2: Validate an email (simplified)
email = "user@example.com"
if re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email):
print("Valid email")
# Task 3: Extract all prices
listing = "Widget A: $12.99, Widget B: $1,234.56, Widget C: $0.99"
prices = re.findall(r'\$[\d,]+\.\d{2}', listing)
print(prices) # ['$12.99', '$1,234.56', '$0.99']
# Task 4: Find dates in multiple formats
log = "2024-01-15 error, 01/15/2024 warning, Jan 15 2024 info"
dates = re.findall(r'\d{4}-\d{2}-\d{2}|\d{2}/\d{2}/\d{4}|[A-Z][a-z]{2} \d{1,2} \d{4}', log)
print(dates) # ['2024-01-15', '01/15/2024', 'Jan 15 2024']The simplest regex is a literal string. It matches exactly that text:
import re
text = "The cat sat on the mat"
# Literal match -- finds the exact substring
print(re.findall(r'cat', text)) # ['cat']
print(re.findall(r'at', text)) # ['at', 'at', 'at'] -- found in cat, sat, mat
print(re.findall(r'dog', text)) # [] -- not found\:# . (dot) matches ANY character except newline
print(re.findall(r'c.t', text)) # ['cat'] -- c, any char, t
# Special characters that need escaping: . ^ $ * + ? { } [ ] \ | ( )
price = "The price is $9.99"
print(re.findall(r'\$\d+\.\d+', price)) # ['$9.99']
# \$ = literal $
# \d+ = one or more digits
# \. = literal dot
# \d+ = one or more digitsCharacter classes match any single character from a set:
# [abc] matches a, b, or c
print(re.findall(r'[cm]at', text)) # ['cat', 'mat']
# [a-z] matches any lowercase letter (range)
print(re.findall(r'[a-z]+', "Hello World 123")) # ['ello', 'orld']
# [A-Za-z] matches any letter
print(re.findall(r'[A-Za-z]+', "Hello World 123")) # ['Hello', 'World']
# [0-9] matches any digit (same as \d)
print(re.findall(r'[0-9]+', "Room 237, Floor 13")) # ['237', '13']
# [^abc] matches any character EXCEPT a, b, c (negation)
print(re.findall(r'[^aeiou ]+', "hello world")) # ['h', 'll', 'w', 'rld']
# Combine ranges
print(re.findall(r'[A-Za-z0-9_]+', "user_name = 42")) # ['user_name', '42']# \d = digit [0-9]
# \D = non-digit [^0-9]
# \w = word char [a-zA-Z0-9_]
# \W = non-word [^a-zA-Z0-9_]
# \s = whitespace [ \t\n\r\f\v]
# \S = non-space [^ \t\n\r\f\v]
# . = any char (except \n by default)
text = "Order #12345 placed on 2024-01-15"
print(re.findall(r'\d+', text)) # ['12345', '2024', '01', '15']
print(re.findall(r'\w+', text)) # ['Order', '12345', 'placed', 'on', '2024', '01', '15']
print(re.findall(r'\S+', text)) # ['Order', '#12345', 'placed', 'on', '2024-01-15']# * = 0 or more
# + = 1 or more
# ? = 0 or 1 (optional)
# {n} = exactly n
# {n,} = n or more
# {n,m} = between n and m (inclusive)
text = "aaa ab abbb ac a"
print(re.findall(r'ab*', text)) # ['a', 'a', 'a', 'ab', 'abbb', 'a', 'a']
# a=0 b's, ab=1 b, abbb=3 b's
print(re.findall(r'ab+', text)) # ['ab', 'abbb'] -- must have at least 1 b
print(re.findall(r'ab?', text)) # ['a', 'a', 'a', 'ab', 'ab', 'a', 'a']
# ? means 0 or 1 b
# Specific counts
phone = "Call 555-1234 or 555-12345678"
print(re.findall(r'\d{3}-\d{4}', phone)) # ['555-1234'] -- exactly 3, dash, exactly 4
print(re.findall(r'\d{3}-\d{4,8}', phone)) # ['555-1234', '555-12345678']# ^ = start of string (or start of line with re.MULTILINE)
# $ = end of string (or end of line with re.MULTILINE)
# \b = word boundary (between \w and \W)
text = "cat caterpillar concatenate scat"
# \b ensures we match whole words
print(re.findall(r'cat', text)) # ['cat', 'cat', 'cat', 'cat'] -- 4 matches
print(re.findall(r'\bcat\b', text)) # ['cat'] -- only the standalone word
# ^ and $ for start/end
lines = "error: disk full\nwarning: low memory\nerror: timeout"
print(re.findall(r'^error', lines)) # ['error'] -- only first line
print(re.findall(r'^error', lines, re.MULTILINE)) # ['error', 'error'] -- both lines
# \b is critical for NLP tokenization
sentence = "I can't believe it's not butter"
words = re.findall(r"\b[\w']+\b", sentence)
print(words) # ["I", "can't", "believe", "it's", "not", "butter"]What does the pattern r'\b\d{3}\b' match in the string '12 123 1234 12345'?
import re
# Without groups: findall returns the full match
text = "2024-01-15 and 2023-12-25"
print(re.findall(r'\d{4}-\d{2}-\d{2}', text))
# ['2024-01-15', '2023-12-25']
# With groups: findall returns only the captured parts
print(re.findall(r'(\d{4})-(\d{2})-(\d{2})', text))
# [('2024', '01', '15'), ('2023', '12', '25')]
# Access groups via match object
match = re.search(r'(\d{4})-(\d{2})-(\d{2})', text)
if match:
print(match.group(0)) # '2024-01-15' -- full match
print(match.group(1)) # '2024' -- first group
print(match.group(2)) # '01' -- second group
print(match.group(3)) # '15' -- third group
print(match.groups()) # ('2024', '01', '15') -- all groups as tupleNamed groups make your regex self-documenting:
# Named groups -- much more readable than group(1), group(2)
pattern = r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})'
match = re.search(pattern, "Event on 2024-03-15")
if match:
print(match.group('year')) # '2024'
print(match.group('month')) # '03'
print(match.group('day')) # '15'
print(match.groupdict()) # {'year': '2024', 'month': '03', 'day': '15'}
# Named groups in finditer (the professional way to iterate matches)
log = "2024-01-15 ERROR disk full | 2024-01-16 WARNING low memory"
for m in re.finditer(pattern, log):
print(f"{m.group('year')}/{m.group('month')}/{m.group('day')}")
# 2024/01/15
# 2024/01/16Sometimes you need grouping for alternation or quantifiers but do not want to capture:
# Without non-capturing group: both parts are captured
text = "I like cats and dogs"
print(re.findall(r'(cat|dog)s', text))
# ['cat', 'dog'] -- captures the group content
# With non-capturing group: only the full match is returned
print(re.findall(r'(?:cat|dog)s', text))
# ['cats', 'dogs'] -- no capture, returns full match
# Useful when you need alternation inside a larger pattern
urls = "Visit http://example.com or https://secure.example.com"
print(re.findall(r'https?://[\w.]+', urls))
# ['http://example.com', 'https://secure.example.com']
# Same thing with explicit non-capturing group
print(re.findall(r'(?:https?://)([\w.]+)', urls))
# ['example.com', 'secure.example.com'] -- captures only the domain# \1 refers to whatever group 1 matched
# Find repeated words (common typo)
text = "The the quick brown fox fox jumped over the the lazy dog"
print(re.findall(r'\b(\w+)\s+\1\b', text, re.IGNORECASE))
# ['The', 'fox', 'the']
# Find matching HTML tags
html = "<b>bold</b> and <i>italic</i>"
print(re.findall(r'<(\w+)>(.*?)</\1>', html))
# [('b', 'bold'), ('i', 'italic')]import re
text = "Hello World, hello python, HELLO REGEX"
# re.search() -- find FIRST match anywhere in string
match = re.search(r'hello', text, re.IGNORECASE)
print(match.group()) # 'Hello'
print(match.start()) # 0
print(match.end()) # 5
print(match.span()) # (0, 5)
# re.match() -- match only at the START of string
match = re.match(r'Hello', text)
print(match.group()) # 'Hello'
match = re.match(r'World', text)
print(match) # None -- 'World' is not at the start
# re.findall() -- find ALL non-overlapping matches
print(re.findall(r'hello', text, re.IGNORECASE))
# ['Hello', 'hello', 'HELLO']
# re.finditer() -- iterator of match objects (use for large texts)
for m in re.finditer(r'hello', text, re.IGNORECASE):
print(f"Found '{m.group()}' at position {m.start()}-{m.end()}")
# Found 'Hello' at position 0-5
# Found 'hello' at position 13-18
# Found 'HELLO' at position 27-32
# re.sub() -- search and replace
print(re.sub(r'hello', 'HI', text, flags=re.IGNORECASE))
# 'HI World, HI python, HI REGEX'
# re.sub() with a function -- dynamic replacements
def censor(match):
return '*' * len(match.group())
print(re.sub(r'\b\w{5,}\b', censor, "The quick brown fox"))
# 'The ***** ***** fox' -- censors words with 5+ characters
# re.split() -- split by pattern (more powerful than str.split)
text = "one,two; three four\tfive"
print(re.split(r'[,;\s]+', text))
# ['one', 'two', 'three', 'four', 'five']What is the difference between re.match() and re.search()?
? after a quantifier makes it lazy -- it matches as little as possible:html = "<b>bold</b> and <i>italic</i>"
# Greedy: .* matches as MUCH as possible
print(re.findall(r'<.*>', html))
# ['<b>bold</b> and <i>italic</i>'] -- matched from first < to LAST >
# Lazy: .*? matches as LITTLE as possible
print(re.findall(r'<.*?>', html))
# ['<b>', '</b>', '<i>', '</i>'] -- matched each tag individually
# Another example: extracting quoted strings
text = 'She said "hello" and he said "goodbye"'
# Greedy: captures from first " to last "
print(re.findall(r'".*"', text))
# ['"hello" and he said "goodbye"']
# Lazy: captures each quoted string
print(re.findall(r'".*?"', text))
# ['"hello"', '"goodbye"']# (?=...) -- positive lookahead: "followed by"
# (?!...) -- negative lookahead: "NOT followed by"
# (?<=...) -- positive lookbehind: "preceded by"
# (?<!...) -- negative lookbehind: "NOT preceded by"
text = "100 USD, 200 EUR, 300 GBP, 400 USD"
# Find amounts followed by USD (without capturing USD)
print(re.findall(r'\d+(?= USD)', text))
# ['100', '400'] -- gets the numbers, not "USD"
# Find amounts NOT followed by USD
print(re.findall(r'\d+(?! USD)', text))
# ['10', '200', '300', '40']
# Note: '10' and '40' appear because regex finds partial matches
# Better version with word boundary:
print(re.findall(r'\b\d+\b(?! USD)', text))
# ['200', '300'] -- only complete numbers not followed by USD
# Lookbehind: find amounts preceded by $
prices = "Items: $50, $100, 200, $75"
print(re.findall(r'(?<=\$)\d+', prices))
# ['50', '100', '75'] -- gets numbers after $, without the $
# Negative lookbehind: find numbers NOT preceded by $
print(re.findall(r'(?<!\$)\b\d+\b', prices))
# ['200'] -- only the number without a $def validate_password(password: str) -> bool:
"""
Password must have:
- At least 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one digit
- At least one special character
"""
pattern = r'^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,}$'
# (?=.*[A-Z]) -- lookahead: has uppercase somewhere
# (?=.*[a-z]) -- lookahead: has lowercase somewhere
# (?=.*\d) -- lookahead: has digit somewhere
# (?=.*[!@#$%^&*]) -- lookahead: has special char somewhere
# .{8,} -- actual match: 8+ of any characters
return bool(re.match(pattern, password))
print(validate_password("Abc12345!")) # True
print(validate_password("abc12345")) # False -- no uppercase or special
print(validate_password("SHORT1!")) # False -- less than 8 charsWhen you use the same pattern multiple times, compile it first:
import re
import time
# Approach 1: Compile once, use many times (fast)
email_pattern = re.compile(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$')
emails = [
"alice@example.com",
"bob@company.co.uk",
"invalid@",
"no-at-sign.com",
"valid.email+tag@gmail.com",
"bad email@space.com",
"good_one@domain.org",
]
# Compiled pattern -- call .match(), .search(), .findall() on the pattern object
valid = [e for e in emails if email_pattern.match(e)]
print(f"Valid emails: {valid}")
# ['alice@example.com', 'bob@company.co.uk', 'valid.email+tag@gmail.com', 'good_one@domain.org']
# Approach 2: Using re.match() each time (slower -- recompiles pattern each call)
# Python does cache recently used patterns, but re.compile() is explicit and clear
valid2 = [e for e in emails if re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', e)]# Compiled patterns are especially useful with multiple patterns
log_patterns = {
'error': re.compile(r'ERROR\s+(.+)'),
'warning': re.compile(r'WARNING\s+(.+)'),
'timestamp': re.compile(r'(\d{4}-\d{2}-\d{2}\s+\d{2}:\d{2}:\d{2})'),
'ip_address': re.compile(r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})'),
}
def parse_log_line(line: str) -> dict:
"""Parse a log line into structured data."""
result = {}
for name, pattern in log_patterns.items():
match = pattern.search(line)
if match:
result[name] = match.group(1)
return result
log_line = "2024-01-15 14:30:22 ERROR Connection refused from 192.168.1.100"
print(parse_log_line(log_line))
# {'error': 'Connection refused from 192.168.1.100',
# 'timestamp': '2024-01-15 14:30:22',
# 'ip_address': '192.168.1.100'}import re
# re.IGNORECASE (or re.I) -- case-insensitive matching
print(re.findall(r'python', 'Python PYTHON python', re.IGNORECASE))
# ['Python', 'PYTHON', 'python']
# re.MULTILINE (or re.M) -- ^ and $ match at line boundaries
text = "error: disk full\nwarning: low memory\nerror: timeout"
print(re.findall(r'^error: .+', text, re.MULTILINE))
# ['error: disk full', 'error: timeout']
# re.DOTALL (or re.S) -- . matches newline too
html = "<div>\n <p>Hello</p>\n</div>"
print(re.findall(r'<div>.*</div>', html)) # [] -- . doesn't match \n
print(re.findall(r'<div>.*</div>', html, re.DOTALL)) # ['<div>\n <p>Hello</p>\n</div>']
# Combine flags with | (bitwise OR)
print(re.findall(r'^error', text, re.IGNORECASE | re.MULTILINE))
# ['error', 'error']import re
def validate_email(email: str) -> bool:
"""
Validate an email address.
This is a simplified pattern. RFC 5322 compliant email regex
is thousands of characters long -- in production, use a library
like email-validator instead.
"""
pattern = re.compile(r'''
^ # start of string
[a-zA-Z0-9] # must start with alphanumeric
[a-zA-Z0-9._%+-]* # followed by valid chars
@ # the @ symbol
[a-zA-Z0-9] # domain starts with alphanumeric
[a-zA-Z0-9.-]* # domain body
\. # dot before TLD
[a-zA-Z]{2,} # TLD: at least 2 letters
$ # end of string
''', re.VERBOSE)
return bool(pattern.match(email))
# Test cases
test_emails = [
("alice@example.com", True),
("bob.smith+tag@company.co.uk", True),
("user@domain.org", True),
("@missing-local.com", False),
("no-at-sign.com", False),
("spaces in@email.com", False),
("missing@tld.", False),
(".starts-with-dot@example.com", False),
]
for email, expected in test_emails:
result = validate_email(email)
status = "PASS" if result == expected else "FAIL"
print(f" {status}: {email!r:40s} -> {result}")def extract_phone_numbers(text: str) -> list[dict]:
"""
Extract phone numbers in various US formats.
Returns structured data with area code, exchange, and number.
"""
pattern = re.compile(r'''
(?: # optional country code
\+?1[\s.-]? # +1 or 1
)?
\(? # optional opening paren
(?P<area>\d{3}) # area code (3 digits)
\)? # optional closing paren
[\s.-]? # optional separator
(?P<exchange>\d{3}) # exchange (3 digits)
[\s.-]? # optional separator
(?P<number>\d{4}) # subscriber number (4 digits)
''', re.VERBOSE)
results = []
for match in pattern.finditer(text):
results.append({
'full': match.group(0).strip(),
'area_code': match.group('area'),
'exchange': match.group('exchange'),
'number': match.group('number'),
})
return results
text = """
Contact us:
Main office: (555) 123-4567
Support: 555.987.6543
Toll-free: +1-800-555-0199
Direct: 5551234567
"""
for phone in extract_phone_numbers(text):
print(f" {phone['full']:20s} -> ({phone['area_code']}) {phone['exchange']}-{phone['number']}")
# (555) 123-4567 -> (555) 123-4567
# 555.987.6543 -> (555) 987-6543
# +1-800-555-0199 -> (800) 555-0199
# 5551234567 -> (555) 123-4567def parse_web_logs(log_text: str) -> list[dict]:
"""
Parse Apache-style web server log entries.
Format: IP - - [timestamp] "METHOD /path HTTP/x.x" status bytes
"""
pattern = re.compile(r'''
(?P<ip>\d{1,3}(?:\.\d{1,3}){3}) # IP address
\s+-\s+-\s+ # two dashes
\[(?P<timestamp>[^\]]+)\] # timestamp in brackets
\s+" # opening quote
(?P<method>\w+) # HTTP method
\s+(?P<path>\S+) # request path
\s+HTTP/[\d.]+" # HTTP version
\s+(?P<status>\d{3}) # status code
\s+(?P<bytes>\d+|-) # response bytes
''', re.VERBOSE)
entries = []
for match in pattern.finditer(log_text):
entries.append(match.groupdict())
return entries
logs = """192.168.1.100 - - [15/Jan/2024:14:30:22 +0000] "GET /api/users HTTP/1.1" 200 1234
10.0.0.50 - - [15/Jan/2024:14:30:23 +0000] "POST /api/login HTTP/1.1" 401 89
192.168.1.100 - - [15/Jan/2024:14:30:24 +0000] "GET /static/style.css HTTP/1.1" 304 0"""
for entry in parse_web_logs(logs):
print(f" {entry['ip']:16s} {entry['method']:5s} {entry['path']:25s} -> {entry['status']}")
# 192.168.1.100 GET /api/users -> 200
# 10.0.0.50 POST /api/login -> 401
# 192.168.1.100 GET /static/style.css -> 304def split_sentences(text: str) -> list[str]:
"""
Split text into sentences.
Handles common abbreviations (Mr., Dr., Prof.) by protecting them
before splitting. This is a simplified version -- production NLP
uses spaCy or NLTK.
Why not use a lookbehind with alternation?
Python's `re` module requires fixed-width lookbehinds, so
`(?<!(?:Mr|Mrs|Prof))` raises an error. The protect/restore trick
below sidesteps that limitation cleanly.
"""
abbreviations = ['Mr', 'Mrs', 'Ms', 'Dr', 'Prof', 'Jr', 'Sr', 'vs', 'etc']
sentinel = '' # an unlikely character
# 1. Protect "Mr.", "Dr.", etc. by replacing the period with a sentinel
protected = text
for abbr in abbreviations:
protected = re.sub(rf'\b{abbr}\.', f'{abbr}{sentinel}', protected)
# 2. Split on sentence-ending punctuation followed by whitespace + uppercase
parts = re.split(r'(?<=[.!?])\s+(?=[A-Z])', protected)
# 3. Restore the sentinel back to a period
return [p.replace(sentinel, '.') for p in parts]
text = """Dr. Smith went to Washington. He met with Prof. Jones at 3:30 p.m. They discussed the U.S. economy. Was it productive? Yes! The meeting went well."""
for i, sentence in enumerate(split_sentences(text), 1):
print(f" {i}. {sentence}")
def tokenize_simple(text: str) -> list[str]:
"""
Simple regex tokenizer for NLP preprocessing.
Splits on whitespace and punctuation while keeping
contractions and hyphenated words together.
"""
pattern = r"""
\w+(?:'\w+)* | # words with optional contractions (don't, it's)
\$?\d+(?:\.\d+)? | # numbers, optionally with decimal and dollar sign
[^\w\s] # individual punctuation characters
"""
return re.findall(pattern, text, re.VERBOSE)
sample = "I can't believe it's only $9.99! That's a 50% discount."
tokens = tokenize_simple(sample)
print(f"\nTokens: {tokens}")
# ["I", "can't", "believe", "it's", "only", "$9.99", "!", "That's", "a", "50", "%", "discount", "."]def strip_html_tags(html: str) -> str:
"""
Remove HTML tags from text, preserving content.
For production HTML parsing, use BeautifulSoup or lxml.
Regex cannot correctly parse nested/malformed HTML (see DeepDive below),
but it works well for simple cases and data cleaning.
"""
# Remove HTML tags
text = re.sub(r'<[^>]+>', '', html)
# Collapse multiple whitespace into single space
text = re.sub(r'\s+', ' ', text)
# Decode common HTML entities
entities = {'&': '&', '<': '<', '>': '>', ' ': ' ', '"': '"'}
for entity, char in entities.items():
text = text.replace(entity, char)
return text.strip()
html = """
<div class="article">
<h1>Machine Learning & AI</h1>
<p>This is a <b>bold</b> statement about <i>deep learning</i>.</p>
<ul>
<li>Neural networks</li>
<li>Transformers</li>
</ul>
</div>
"""
print(strip_html_tags(html))
# 'Machine Learning & AI This is a bold statement about deep learning. Neural networks Transformers'Tests · Test CSV parsing with quoted fields containing commas. Test URL extraction excludes ftp:// URLs. Test data cleaning normalizes casing and whitespace.
Here is a compact reference for the most common regex syntax. Bookmark this section.
# ╔══════════════════════════════════════════════════════════════════╗
# ║ REGEX QUICK REFERENCE ║
# ╠══════════════════════════════════════════════════════════════════╣
# ║ ║
# ║ CHARACTER CLASSES ║
# ║ . any character (except \n) ║
# ║ \d digit [0-9] ║
# ║ \D non-digit [^0-9] ║
# ║ \w word char [a-zA-Z0-9_] ║
# ║ \W non-word [^a-zA-Z0-9_] ║
# ║ \s whitespace [ \t\n\r\f\v] ║
# ║ \S non-whitespace ║
# ║ [abc] any of a, b, c ║
# ║ [^abc] none of a, b, c ║
# ║ [a-z] range a to z ║
# ║ ║
# ║ QUANTIFIERS ║
# ║ * 0 or more (greedy) ║
# ║ + 1 or more (greedy) ║
# ║ ? 0 or 1 (optional) ║
# ║ {n} exactly n ║
# ║ {n,} n or more ║
# ║ {n,m} between n and m ║
# ║ *? +? lazy versions (match as little as possible) ║
# ║ ║
# ║ ANCHORS ║
# ║ ^ start of string/line ║
# ║ $ end of string/line ║
# ║ \b word boundary ║
# ║ ║
# ║ GROUPS ║
# ║ (...) capturing group ║
# ║ (?P<name>...) named group ║
# ║ (?:...) non-capturing group ║
# ║ \1, \2 backreference to group 1, 2 ║
# ║ ║
# ║ LOOKAROUND ║
# ║ (?=...) positive lookahead ║
# ║ (?!...) negative lookahead ║
# ║ (?<=...) positive lookbehind ║
# ║ (?<!...) negative lookbehind ║
# ║ ║
# ║ FLAGS ║
# ║ re.IGNORECASE (re.I) case-insensitive ║
# ║ re.MULTILINE (re.M) ^ $ match line boundaries ║
# ║ re.DOTALL (re.S) . matches \n ║
# ║ re.VERBOSE (re.X) allow comments and whitespace ║
# ║ ║
# ║ re MODULE FUNCTIONS ║
# ║ re.search(pat, text) first match anywhere ║
# ║ re.match(pat, text) match at start only ║
# ║ re.fullmatch(pat, text) match entire string ║
# ║ re.findall(pat, text) all non-overlapping matches ║
# ║ re.finditer(pat, text) iterator of match objects ║
# ║ re.sub(pat, repl, text) search and replace ║
# ║ re.split(pat, text) split by pattern ║
# ║ re.compile(pat) compile for reuse ║
# ╚══════════════════════════════════════════════════════════════════╝Interactive Lab
See how text gets split into tokens — the same pattern-matching logic that powers regex
str.find() and str.replace() cannot express the pattern you need. Character classes (\d, \w, [a-z]), quantifiers (+, *, {n,m}), and anchors (^, $, \b) are the fundamental building blocks() to capture parts of a match, (?P<name>...) for readable named groups, and (?:...) when you need grouping without capturing. re.finditer() with named groups is the professional way to parse textWhat does the regex pattern r'\b\w+ing\b' match?
re.search()re.fullmatch().* grabs everything it can; .*? grabs as little as possible. For extracting content between delimiters (quotes, tags, brackets), lazy matching is almost always what you want(a+)+ create exponential worst-case behavior. Keep patterns simple, avoid nesting * and + inside groups with * or +, and use re.compile() with timeout awareness for user-supplied patterns