Quantifiers and Anchors¶
Quantifiers¶
Quantifiers specify how many times the preceding element must occur for a match. By default, quantifiers are greedy — they match as much text as possible.
Basic Quantifiers¶
| Quantifier | Meaning | Example | Matches |
|---|---|---|---|
* |
Zero or more | ab*c |
ac, abc, abbc, abbbc |
+ |
One or more | ab+c |
abc, abbc, abbbc (not ac) |
? |
Zero or one | colou?r |
color, colour |
{n} |
Exactly n | \d{4} |
2024 (exactly 4 digits) |
{n,} |
At least n | \d{2,} |
42, 007, 12345 |
{n,m} |
Between n and m | \d{2,4} |
42, 007, 2024 |
import re
# * — zero or more
re.findall(r'go*d', 'gd god good goood')
# ['gd', 'god', 'good', 'goood']
# + — one or more
re.findall(r'go+d', 'gd god good goood')
# ['god', 'good', 'goood']
# ? — zero or one
re.findall(r'colou?r', 'color and colour')
# ['color', 'colour']
# {n} — exactly n
re.findall(r'\b\d{3}\b', '1 12 123 1234')
# ['123']
# {n,m} — between n and m
re.findall(r'\b\d{2,4}\b', '1 12 123 1234 12345')
# ['12', '123', '1234']
Greedy vs Lazy Quantifiers¶
By default, quantifiers are greedy — they consume as much text as possible. Adding ? after a quantifier makes it lazy (also called non-greedy or reluctant) — it matches as little as possible.
| Greedy | Lazy | Behavior |
|---|---|---|
* |
*? |
Zero or more (prefer fewer) |
+ |
+? |
One or more (prefer fewer) |
? |
?? |
Zero or one (prefer zero) |
{n,m} |
{n,m}? |
Between n and m (prefer fewer) |
import re
html = '<b>bold</b> and <i>italic</i>'
# Greedy — matches from first < to LAST >
re.findall(r'<.*>', html)
# ['<b>bold</b> and <i>italic</i>']
# Lazy — matches from first < to NEXT >
re.findall(r'<.*?>', html)
# ['<b>', '</b>', '<i>', '</i>']
This distinction is critical when parsing structured text:
import re
text = '"first" and "second" and "third"'
# Greedy: matches from first " to last "
re.findall(r'".*"', text)
# ['"first" and "second" and "third"']
# Lazy: matches each quoted string
re.findall(r'".*?"', text)
# ['"first"', '"second"', '"third"']
When to Use Lazy Quantifiers
Use lazy quantifiers when you want to match the shortest possible substring, especially with delimiters like quotes, tags, or brackets. For simple patterns without ambiguity, greedy quantifiers work fine.
Possessive Quantifiers (Python 3.11+)¶
Python 3.11 introduced possessive quantifiers (*+, ++, ?+, {n,m}+). These are greedy but never backtrack, which can improve performance:
import re
# Possessive + (Python 3.11+)
# Fails fast — no backtracking
try:
re.search(r'[a-z]++[a-z]', 'abcdef') # None — possessive consumed all
except Exception:
pass # Older Python versions
Possessive quantifiers are an optimization tool; in most cases, greedy and lazy are sufficient.
Anchors¶
Anchors match positions in the string, not characters. They have zero width — they don't consume any characters.
String Anchors¶
| Anchor | Matches |
|---|---|
^ |
Start of string (or line with re.M) |
$ |
End of string (or line with re.M) |
\A |
Start of string (ignores re.M) |
\Z |
End of string (ignores re.M) |
import re
text = "line one\nline two\nline three"
# ^ matches start of string only
re.findall(r'^line', text)
# ['line']
# ^ with MULTILINE matches start of each line
re.findall(r'^line', text, re.M)
# ['line', 'line', 'line']
# \A always matches start of string, regardless of flags
re.findall(r'\Aline', text, re.M)
# ['line']
Word Boundaries¶
| Anchor | Matches |
|---|---|
\b |
Boundary between word and non-word character |
\B |
Position that is not a word boundary |
import re
text = "cat concatenate scattered"
# \b — word boundary
re.findall(r'\bcat\b', text) # ['cat']
re.findall(r'\bcat', text) # ['cat', 'cat']
# \B — NOT a word boundary
re.findall(r'\Bcat', text) # ['cat'] — the 'cat' inside 'scattered'
re.findall(r'cat\B', text) # ['cat', 'cat'] — 'cat' not at end of word
Word boundaries are essential for matching whole words:
import re
text = "I like Java but not JavaScript"
# Without boundary — matches both
re.findall(r'Java', text)
# ['Java', 'Java']
# With boundary — matches only the standalone word
re.findall(r'\bJava\b', text)
# ['Java']
Combining Quantifiers and Anchors¶
Quantifiers and anchors work together to create precise patterns:
import re
# Match lines that contain only digits
text = "123\nabc\n456\na1b"
re.findall(r'^\d+$', text, re.M)
# ['123', '456']
# Validate a string is exactly 5 uppercase letters
def is_valid_code(s):
return bool(re.fullmatch(r'[A-Z]{5}', s))
print(is_valid_code("HELLO")) # True
print(is_valid_code("Hello")) # False
print(is_valid_code("HELLOO")) # False
Validating Input Formats¶
import re
# Simple integer validation (optional sign)
def is_integer(s):
return bool(re.fullmatch(r'[+-]?\d+', s))
print(is_integer("42")) # True
print(is_integer("-17")) # True
print(is_integer("3.14")) # False
# Simple float validation
def is_float(s):
return bool(re.fullmatch(r'[+-]?\d*\.?\d+', s))
print(is_float("3.14")) # True
print(is_float(".5")) # True
print(is_float("42")) # True
print(is_float("")) # False
Summary¶
| Concept | Key Takeaway |
|---|---|
* + ? |
Zero+, one+, zero-or-one repetitions |
{n} {n,m} |
Exact or range repetitions |
| Greedy | Default — match as much as possible |
Lazy (*? +?) |
Match as little as possible |
^ / $ |
Start/end of string (or line with re.M) |
\A / \Z |
Start/end of string (always, ignoring re.M) |
\b / \B |
Word boundary / not a word boundary |
re.fullmatch() |
Anchor the entire string (like ^...$) |
Runnable Example: quantifiers_tutorial.py¶
"""
Python Regular Expressions - Tutorial 03: Quantifiers
=====================================================
LEARNING OBJECTIVES:
-------------------
1. Understand repetition with quantifiers (*, +, ?, {m,n})
2. Learn the difference between greedy and non-greedy matching
3. Apply quantifiers to character classes
4. Use quantifiers for practical pattern matching
5. Combine quantifiers with previously learned concepts
PREREQUISITES:
-------------
- Tutorial 01: Regex Basics
- Tutorial 02: Character Classes
DIFFICULTY: INTERMEDIATE
"""
import re
# ==============================================================================
# SECTION 1: INTRODUCTION TO QUANTIFIERS
# ==============================================================================
if __name__ == "__main__":
"""
QUANTIFIERS specify how many times a pattern should match.
Instead of matching a single character, you can match multiple repetitions.
Basic Quantifiers:
* : 0 or more occurrences (zero to infinity)
+ : 1 or more occurrences (at least one)
? : 0 or 1 occurrence (optional)
{m} : Exactly m occurrences
{m,n} : Between m and n occurrences (inclusive)
{m,} : m or more occurrences
Quantifiers apply to the character or group immediately before them.
"""
print("="*70)
print("SECTION 1: BASIC QUANTIFIERS")
print("="*70)
# Example 1: The * quantifier (zero or more)
# ------------------------------------------
# * matches the preceding element 0 or more times
pattern1 = r"ab*c" # 'a', followed by zero or more 'b', followed by 'c'
test_strings1 = ["ac", "abc", "abbc", "abbbc", "axc"]
print(f"Pattern: '{pattern1}' (a + zero or more b + c)")
for text in test_strings1:
match = re.search(pattern1, text)
result = f"✓ Matches" if match else "✗ No match"
print(f" '{text}': {result}")
print()
# Example 2: The + quantifier (one or more)
# -----------------------------------------
# + matches the preceding element 1 or more times
pattern2 = r"ab+c" # 'a', followed by one or more 'b', followed by 'c'
test_strings2 = ["ac", "abc", "abbc", "abbbc"]
print(f"Pattern: '{pattern2}' (a + one or more b + c)")
for text in test_strings2:
match = re.search(pattern2, text)
result = f"✓ Matches" if match else "✗ No match"
print(f" '{text}': {result}")
print()
# Example 3: The ? quantifier (optional)
# --------------------------------------
# ? makes the preceding element optional (0 or 1)
pattern3 = r"colou?r" # 'colo', followed by optional 'u', followed by 'r'
test_strings3 = ["color", "colour", "colouur"]
print(f"Pattern: '{pattern3}' (optional 'u')")
for text in test_strings3:
match = re.search(pattern3, text)
result = f"✓ Matches" if match else "✗ No match"
print(f" '{text}': {result}")
print()
# ==============================================================================
# SECTION 2: QUANTIFIERS WITH CHARACTER CLASSES
# ==============================================================================
"""
Quantifiers work great with character classes!
This is where their real power shines.
"""
print("="*70)
print("SECTION 2: QUANTIFIERS WITH CHARACTER CLASSES")
print("="*70)
# Example 4: Matching multiple digits
# -----------------------------------
pattern4 = r"\d+" # One or more digits
text4 = "I have 3 cats, 12 dogs, and 100 fish"
numbers = re.findall(pattern4, text4)
print(f"Text: '{text4}'")
print(f"Pattern: '\\d+' (one or more digits)")
print(f"Numbers found: {numbers}")
print()
# Example 5: Matching multiple word characters
# --------------------------------------------
pattern5 = r"\w+" # One or more word characters (forms words)
text5 = "Hello, World! This is Python-3.9"
words = re.findall(pattern5, text5)
print(f"Text: '{text5}'")
print(f"Pattern: '\\w+' (one or more word chars)")
print(f"Words found: {words}")
print()
# Example 6: Matching optional whitespace
# ---------------------------------------
pattern6 = r"\d+\s*\w+" # Digits, optional spaces, then word
text6 = "3cats 12 dogs 100 fish"
matches = re.findall(pattern6, text6)
print(f"Text: '{text6}'")
print(f"Pattern: '\\d+\\s*\\w+' (number, optional spaces, word)")
print(f"Matches: {matches}")
print()
# ==============================================================================
# SECTION 3: SPECIFIC REPETITION WITH {m,n}
# ==============================================================================
"""
CURLY BRACES allow you to specify exact repetition counts:
{m} : Exactly m times
{m,n} : From m to n times (inclusive)
{m,} : m or more times
{,n} : Up to n times (0 to n)
"""
print("="*70)
print("SECTION 3: SPECIFIC REPETITION COUNTS")
print("="*70)
# Example 7: Exactly m occurrences {m}
# ------------------------------------
pattern7 = r"\d{3}" # Exactly 3 digits
text7 = "1 12 123 1234"
matches = re.findall(pattern7, text7)
print(f"Text: '{text7}'")
print(f"Pattern: '\\d{{3}}' (exactly 3 digits)")
print(f"Matches: {matches}")
print()
# Example 8: Range {m,n}
# ----------------------
pattern8 = r"\d{2,4}" # Between 2 and 4 digits
text8 = "1 12 123 1234 12345"
matches = re.findall(pattern8, text8)
print(f"Text: '{text8}'")
print(f"Pattern: '\\d{{2,4}}' (2 to 4 digits)")
print(f"Matches: {matches}")
print()
# Example 9: Minimum repetitions {m,}
# -----------------------------------
pattern9 = r"\d{3,}" # 3 or more digits
text9 = "1 12 123 1234 12345"
matches = re.findall(pattern9, text9)
print(f"Text: '{text9}'")
print(f"Pattern: '\\d{{3,}}' (3 or more digits)")
print(f"Matches: {matches}")
print()
# Example 10: Phone number matching
# ---------------------------------
# US phone number: XXX-XXX-XXXX
pattern10 = r"\d{3}-\d{3}-\d{4}"
text10 = "Call me at 555-123-4567 or 800-555-0199"
phone_numbers = re.findall(pattern10, text10)
print(f"Text: '{text10}'")
print(f"Pattern: '\\d{{3}}-\\d{{3}}-\\d{{4}}' (phone format)")
print(f"Phone numbers: {phone_numbers}")
print()
# ==============================================================================
# SECTION 4: GREEDY VS NON-GREEDY MATCHING
# ==============================================================================
"""
GREEDY MATCHING (default):
- Quantifiers (*, +, {m,n}) match as MUCH text as possible
- They try to consume the maximum amount of characters
NON-GREEDY (or LAZY) MATCHING:
- Add ? after the quantifier: *?, +?, {m,n}?
- They match as LITTLE text as possible
- They try to consume the minimum amount of characters
"""
print("="*70)
print("SECTION 4: GREEDY VS NON-GREEDY MATCHING")
print("="*70)
# Example 11: Greedy matching demonstration
# -----------------------------------------
text11 = "<html><head><title>Page</title></head></html>"
# Greedy: matches from first < to last >
greedy_pattern = r"<.*>"
greedy_match = re.search(greedy_pattern, text11)
print(f"Text: '{text11}'")
print(f"Greedy pattern: '<.*>' (. repeated, greedy)")
if greedy_match:
print(f"Matched: '{greedy_match.group()}'")
print("(Matches everything from first < to last >)")
print()
# Example 12: Non-greedy matching demonstration
# ---------------------------------------------
# Non-greedy: matches from < to the nearest >
non_greedy_pattern = r"<.*?>"
non_greedy_matches = re.findall(non_greedy_pattern, text11)
print(f"Text: '{text11}'")
print(f"Non-greedy pattern: '<.*?>' (. repeated, non-greedy)")
print(f"All matches: {non_greedy_matches}")
print("(Each match is minimal - from < to nearest >)")
print()
# Example 13: Practical example - extracting quoted strings
# ---------------------------------------------------------
text13 = 'He said "Hello" and then "Goodbye"'
# Greedy version - wrong!
greedy_quotes = r'".*"'
greedy_result = re.search(greedy_quotes, text13)
print(f"Text: {text13}")
print(f"\nGreedy '\".*\"':")
if greedy_result:
print(f" Matched: {greedy_result.group()}")
print(" (Oops! Got everything from first to last quote)")
# Non-greedy version - correct!
non_greedy_quotes = r'".*?"'
non_greedy_results = re.findall(non_greedy_quotes, text13)
print(f"\nNon-greedy '\".*?\"':")
print(f" Matched: {non_greedy_results}")
print(" (Perfect! Got each quoted string separately)")
print()
# ==============================================================================
# SECTION 5: COMBINING QUANTIFIERS
# ==============================================================================
print("="*70)
print("SECTION 5: COMBINING QUANTIFIERS")
print("="*70)
# Example 14: Complex pattern with multiple quantifiers
# -----------------------------------------------------
# Match: one or more letters, optional spaces, one or more digits
pattern14 = r"[a-zA-Z]+\s*\d+"
text14 = "Room123 Building 456 Floor7"
matches = re.findall(pattern14, text14)
print(f"Text: '{text14}'")
print(f"Pattern: '[a-zA-Z]+\\s*\\d+' (letters, optional space, digits)")
print(f"Matches: {matches}")
print()
# Example 15: Email pattern (simplified)
# --------------------------------------
# Basic email: word chars, optional dots/underscores, @, domain
pattern15 = r"[\w.]+@\w+\.\w+"
text15 = "Contact: john.doe@example.com or alice_smith@test.org"
emails = re.findall(pattern15, text15)
print(f"Text: '{text15}'")
print(f"Pattern: '[\\w.]+@\\w+\\.\\w+' (simple email)")
print(f"Emails: {emails}")
print()
# Example 16: URL pattern (very simplified)
# -----------------------------------------
# Basic URL: http(s)://, optional www., domain, optional path
pattern16 = r"https?://(?:www\.)?[\w.-]+\.[\w]+"
text16 = "Visit https://example.com or http://www.test.org"
urls = re.findall(pattern16, text16)
print(f"Text: '{text16}'")
print(f"URLs found: {urls}")
print()
# ==============================================================================
# SECTION 6: PRACTICAL APPLICATIONS
# ==============================================================================
print("="*70)
print("SECTION 6: PRACTICAL APPLICATIONS")
print("="*70)
# Example 17: Extracting prices
# -----------------------------
text17 = "Items: $12.99, $5.50, $100.00, and $0.99"
# Pattern: dollar sign, digits, dot, two digits
price_pattern = r"\$\d+\.\d{2}"
prices = re.findall(price_pattern, text17)
print(f"Text: '{text17}'")
print(f"Prices found: {prices}")
print()
# Example 18: Matching dates (MM/DD/YYYY)
# ---------------------------------------
text18 = "Events: 01/15/2024, 12/31/2023, 06/01/2024"
# Pattern: 2 digits / 2 digits / 4 digits
date_pattern = r"\d{2}/\d{2}/\d{4}"
dates = re.findall(date_pattern, text18)
print(f"Text: '{text18}'")
print(f"Dates found: {dates}")
print()
# Example 19: Extracting hashtags
# -------------------------------
text19 = "Love this! #python #coding #AI #MachineLearning"
# Pattern: # followed by word characters
hashtag_pattern = r"#\w+"
hashtags = re.findall(hashtag_pattern, text19)
print(f"Text: '{text19}'")
print(f"Hashtags: {hashtags}")
print()
# Example 20: Validating password strength
# ----------------------------------------
def check_password_strength(password):
"""
Check if password meets minimum requirements:
- At least 8 characters
- Contains at least one digit
- Contains at least one letter
"""
# Check length
if len(password) < 8:
return False, "Too short (minimum 8 characters)"
# Check for at least one digit
if not re.search(r"\d", password):
return False, "Must contain at least one digit"
# Check for at least one letter
if not re.search(r"[a-zA-Z]", password):
return False, "Must contain at least one letter"
return True, "Password is strong enough"
# Test passwords
test_passwords = ["pass", "password", "pass123", "Pass123!", "12345678"]
print("Password strength check:")
for pwd in test_passwords:
valid, message = check_password_strength(pwd)
status = "✓" if valid else "✗"
print(f" {status} '{pwd}': {message}")
print()
# ==============================================================================
# SECTION 7: COMMON PATTERNS
# ==============================================================================
print("="*70)
print("SECTION 7: COMMON PATTERNS WITH QUANTIFIERS")
print("="*70)
common_patterns = {
"Integer": r"\d+",
"Decimal number": r"\d+\.\d+",
"Word": r"\w+",
"Line of text": r".+",
"Whitespace sequence": r"\s+",
"Variable name": r"[a-zA-Z_]\w*",
"HTML tag": r"<\w+>",
"IP address (simplified)": r"\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}",
}
print("Common regex patterns:")
for name, pattern in common_patterns.items():
print(f" {name:25} : {pattern}")
print()
# ==============================================================================
# SECTION 8: PERFORMANCE CONSIDERATIONS
# ==============================================================================
print("="*70)
print("SECTION 8: PERFORMANCE AND BEST PRACTICES")
print("="*70)
"""
PERFORMANCE TIPS:
1. Be as specific as possible
- Use \d instead of [0-9]
- Use \w instead of [a-zA-Z0-9_]
2. Avoid excessive backtracking
- BAD: (a+)* (nested quantifiers can be slow)
- GOOD: a+ (single quantifier)
3. Use non-greedy when appropriate
- Helps avoid catastrophic backtracking
- More predictable behavior
4. Compile patterns for reuse
- Use re.compile() for patterns used multiple times
5. Use specific quantifiers
- {3,5} is better than .{1,100} when you know the range
"""
# Example 21: Compiling patterns for better performance
# -----------------------------------------------------
import time
# Pattern to match email addresses
email_pattern_string = r"[\w.+-]+@[\w-]+\.[\w.-]+"
# Sample text with many emails
text21 = "emails: " + " ".join([f"user{i}@example.com" for i in range(1000)])
# Method 1: Without compilation
start = time.time()
matches1 = re.findall(email_pattern_string, text21)
time1 = time.time() - start
# Method 2: With compilation
compiled_pattern = re.compile(email_pattern_string)
start = time.time()
matches2 = compiled_pattern.findall(text21)
time2 = time.time() - start
print("Performance comparison (finding 1000 emails):")
print(f" Without compilation: {time1:.6f} seconds")
print(f" With compilation: {time2:.6f} seconds")
print(f" Speedup: {time1/time2:.2f}x faster")
print()
# ==============================================================================
# SECTION 9: SUMMARY
# ==============================================================================
print("="*70)
print("QUANTIFIER CHEAT SHEET")
print("="*70)
cheat_sheet = """
BASIC QUANTIFIERS:
* 0 or more (greedy)
+ 1 or more (greedy)
? 0 or 1 (optional)
{m} Exactly m times
{m,n} m to n times (inclusive)
{m,} m or more times
{,n} Up to n times
NON-GREEDY (LAZY) VERSIONS:
*? 0 or more (non-greedy)
+? 1 or more (non-greedy)
?? 0 or 1 (non-greedy)
{m,n}? m to n times (non-greedy)
COMMON COMBINATIONS:
\d+ One or more digits (number)
\w+ One or more word characters (word)
\s+ One or more whitespace
.* Any characters (greedy)
.*? Any characters (non-greedy)
[a-z]+ One or more lowercase letters
\d{3} Exactly 3 digits
\d{2,4} 2 to 4 digits
KEY PRINCIPLES:
1. Quantifiers apply to the element immediately before them
2. Greedy quantifiers match as much as possible
3. Non-greedy quantifiers match as little as possible
4. Use specific quantifiers when you know the count
5. Compile patterns for repeated use
"""
print(cheat_sheet)
# ==============================================================================
# PRACTICE EXERCISES
# ==============================================================================
print("="*70)
print("PRACTICE CHALLENGES")
print("="*70)
"""
Try these exercises:
1. Match all words with 3-5 letters
2. Extract all numbers (integers and decimals) from text
3. Find all hashtags that are at least 3 characters long
4. Match phone numbers in format: (XXX) XXX-XXXX
5. Extract all HTML/XML tags (both opening and closing)
6. Find all URLs starting with http:// or https://
7. Match valid variable names (start with letter/underscore, then word chars)
8. Extract all quoted strings (handle both single and double quotes)
9. Find all email addresses in a document
10. Match credit card numbers (XXXX-XXXX-XXXX-XXXX or 16 digits)
Solutions in exercises_02_intermediate.py
"""
# ==============================================================================
# END OF TUTORIAL 03
# ==============================================================================
print("\n" + "="*70)
print("END OF TUTORIAL - Quantifiers mastered!")
print("Next: Tutorial 04 - Anchors and Boundaries")
print("="*70)