Python · SQL · Web Dev · Java · AI/ML tracks launching soon — your one platform for all of IT
Advanced+200 XP

Regular Expressions with re

What regex is actually for, when it's overkill, the re module built up systematically, and a real log-parsing example.

50 min August 2026
// Part 01 — Welcome to Phase 5

Advanced Python Starts Here

This module opens Phase 5 — Advanced Python, the most demanding phase of this track before you move into production-readiness and career topics in Phase 6. Everything up to this point — variables, control flow, functions, data structures, files, exceptions, object-oriented Python — was about writing correct programs. Phase 5 is about writing programs that hold up under real-world pressure: text that does not arrive in a clean shape, time zones that do not line up, work that needs to happen concurrently instead of one line at a time, and code whose types and behaviour need to survive contact with other engineers and other systems. Regular expressions are the right place to start, because pattern matching in text turns out to be a dependency of almost everything else in this phase — log parsing, API response cleanup, and data validation all lean on it constantly.

A regular expression (regex, for short) is a small, dense language for describing a pattern that text either matches or does not. Instead of writing loops and conditionals to inspect a string character by character, you describe the shape you are looking for once, and Python's re module does the character-by-character work for you. Regex shows up constantly in real engineering work: validating that a string looks like an email address before accepting it, pulling a request ID out of a log line, splitting a messy CSV field on multiple possible delimiters, or finding every URL inside a block of text.

A first taste — is this a valid-looking US zip code?
import re

zip_code = "80202"

if re.match(r"^\d{5}$", zip_code):
    print("Looks like a valid 5-digit zip code")
else:
    print("Not a valid zip code shape")

# "Looks like a valid 5-digit zip code"

That single pattern — ^\d{5}$ — says "exactly five digits, and nothing else." By the end of this module you will be able to read that immediately. Regex syntax looks intimidating in bulk, but it is built from a genuinely small set of building blocks combined together — this module introduces them one at a time, in order, so nothing feels like it appeared from nowhere.

// Part 02 — When Regex Is (and Isn't) the Right Tool

An Honest Take Before You Learn the Syntax

Before diving into syntax, it is worth being direct about something most tutorials skip: regex is frequently the wrong tool, reached for out of habit rather than necessity. You already know Module 04's string methods — .startswith(), .endswith(), .split(), in — and Module 14's string-processing techniques. For a huge share of everyday text checks, those are faster to write, faster to read, and faster to execute than a regex.

When plain string methods are simply better
# Checking a prefix — regex is unnecessary here
if filename.startswith("invoice_"):      # clear
    ...
# vs
if re.match(r"^invoice_", filename):      # works, but adds no value over the line above

# Checking for a substring — same story
if "error" in log_line.lower():           # clear and fast
    ...
# vs
if re.search(r"error", log_line, re.IGNORECASE):   # heavier than it needs to be

Reach for regex specifically when the pattern you are matching has real structure that plain string methods cannot express — "one or more digits, optionally followed by a decimal point and more digits," or "a sequence of letters, then a dash, then exactly four digits." That is a genuinely different kind of problem than "does this string start with a fixed prefix," and it is exactly the kind of problem this module is about.

🎯 Pro Tip
A useful rule of thumb from experienced engineers: if you can describe what you are matching in one plain sentence using only "starts with," "ends with," "contains," or "equals," write it with string methods. If your sentence needs "one or more," "any digit," "optionally," or "any of these characters," reach for re.
// Part 03 — The Core re Functions

match, search, findall, sub, split

The re module is part of Python's standard library — no installation required. Five functions cover the overwhelming majority of real regex usage, and it is worth learning the difference between them precisely, because mixing them up is one of the most common regex mistakes in real code.

re.match — only checks the START of the string
import re

re.match(r"\d+", "42 apples")     # matches — "42" is found at position 0
re.match(r"\d+", "I have 42 apples")  # None — the string does not START with digits
re.search — checks the WHOLE string for the first match, anywhere
re.search(r"\d+", "I have 42 apples")   # matches — finds "42" anywhere in the string
re.search(r"\d+", "no numbers here")     # None — no digits anywhere
re.findall — returns EVERY match as a list
re.findall(r"\d+", "I have 42 apples and 7 oranges")
# ['42', '7'] — a plain list of strings, every match found
re.sub — find and replace using a pattern
re.sub(r"\d+", "#", "I have 42 apples and 7 oranges")
# "I have # apples and # oranges"

# Redacting something sensitive — a real, common use
re.sub(r"\d{3}-\d{2}-\d{4}", "XXX-XX-XXXX", "SSN on file: 123-45-6789")
# "SSN on file: XXX-XX-XXXX"
re.split — split a string on a pattern, not just a fixed character
re.split(r"[,;]\s*", "apples, oranges;bananas,  grapes")
# ['apples', 'oranges', 'bananas', 'grapes']
# str.split(",") alone could not handle the mix of commas AND semicolons

Notice the recurring shape: match and search return a Match object (or None if nothing matched) — not the matched text directly. findall and split return plain lists. sub returns a new string. Getting the actual matched substring out of a Match object is covered in Part 06 below, once groups are introduced.

⚠️ Important
A match object is truthy, but it is not the matched text. if re.search(r"\d+", text): works because a Match object is always truthy and None is always falsy — but printing that match object directly gives you something like <re.Match object; span=(7, 9), match='42'>, not "42". You need .group() to extract the actual text, shown in Part 06.
// Part 04 — Pattern Syntax, Built Up Systematically

From Literal Characters to Character Classes

Literal characters

Most characters in a regex pattern simply match themselves. The pattern cat matches the literal text "cat" wherever it appears — nothing special is happening yet.

Plain literal matching
re.search(r"cat", "concatenate")   # matches — "cat" appears inside "conCATenate"

The dot . — any single character

. matches exactly one character of any kind (except a newline, by default).

The dot matches anything, once
re.findall(r"c.t", "cat cot cut cят c t")
# ['cat', 'cot', 'cut', 'c t'] — "cят" is skipped, since я and т are TWO characters, not one

Character classes [ ] — any ONE character from a set

Square brackets define a set of acceptable characters for one position. A hyphen inside brackets defines a range, and a leading ^ inside the brackets negates the set.

Character classes
re.findall(r"[aeiou]", "hello world")   # ['e', 'o', 'o']   — any single vowel
re.findall(r"[a-z]", "Hi 123")           # ['i']              — lowercase a through z only
re.findall(r"[A-Za-z0-9]", "Hi 123!")    # ['H','i','1','2','3']  — letters and digits, not '!' or the space
re.findall(r"[^0-9]", "abc123")          # ['a','b','c']       — ^ inside [] means NOT these characters

Shorthand classes: \d \w \s and their negations

Because digit, word-character, and whitespace classes are so common, re provides shorthand for them — and for their opposites.

Shorthand character classes
\d   any digit           — equivalent to [0-9]
\D   any NON-digit        — equivalent to [^0-9]
\w   any "word" character — letters, digits, and underscore, equivalent to [A-Za-z0-9_]
\W   any NON-word character
\s   any whitespace       — space, tab, newline
\S   any NON-whitespace character
Shorthand classes in practice
re.findall(r"\d", "Room 204, Suite 5B")     # ['2','0','4','5']
re.findall(r"\w+", "user_name-42 field!")   # ['user_name', '42', 'field'] — the dash and ! break \w+
// Part 05 — Quantifiers and Anchors

How Many Times, and Where in the String

On their own, character classes only match one character. Quantifiers say how many times the preceding element is allowed to repeat, and anchors pin a match to a specific position in the string rather than letting it appear anywhere.

The quantifiers
*       zero or more
+       one or more
?       zero or one (optional)
{n}     exactly n times
{n,}    n or more times
{n,m}   between n and m times, inclusive
Quantifiers in practice
re.findall(r"\d+", "room 4, hall 12, gate 007")
# ['4', '12', '007'] — \d+ greedily grabs runs of one or more digits

re.match(r"colou?r", "color")    # matches — the 'u' is optional
re.match(r"colou?r", "colour")   # matches too

re.match(r"\d{3}-\d{4}", "555-1234")   # matches — exactly 3 digits, dash, exactly 4 digits
re.match(r"\d{3}-\d{4}", "55-1234")     # None    — only 2 digits before the dash

Anchors — ^ and $

^ anchors a match to the start of the string, and $ anchors it to the end. Without anchors, a pattern is free to match anywhere inside the string — which is a common source of bugs when a full-string validation was actually intended.

Why anchors matter — validating a whole string, not a substring
# WITHOUT anchors — this "validation" actually just checks for digits ANYWHERE
re.match(r"\d{5}", "hello 80202 world extra text")
# matches "80202" — but re.match only pins the START, not the end,
# so trailing junk after a valid-looking prefix is silently accepted

# WITH both anchors — this genuinely validates the ENTIRE string
re.match(r"^\d{5}$", "80202")            # matches — the whole string is exactly 5 digits
re.match(r"^\d{5}$", "80202-extra")      # None    — correctly rejected
⚠️ Important
re.match already anchors to the start — but never the end. A very common bug: assuming re.match(r"\d{5}", value) validates that value is exactly five digits. It does not — it only confirms the string starts with five digits, and anything can follow. Always add a trailing $ when the intent is full-string validation, as shown above.
// Part 06 — Capturing Groups

Pulling Structured Pieces Out of a Match

Parentheses ( ) in a pattern create a capturing group — a piece of the overall match that you can extract individually afterward, rather than only getting the full matched text back as one block. This is where regex stops being a yes/no check and starts being a real parsing tool.

Extracting pieces with groups
import re

text = "Order #4471 placed on 2026-08-13"
match = re.search(r"Order #(\d+) placed on (\d{4}-\d{2}-\d{2})", text)

if match:
    print(match.group(0))   # the FULL match: "Order #4471 placed on 2026-08-13"
    print(match.group(1))   # the FIRST group: "4471"
    print(match.group(2))   # the SECOND group: "2026-08-13"

Named groups — clearer than counting parentheses

Counting group(1), group(2), and so on becomes error-prone once a pattern has more than two or three groups, especially after the pattern gets edited later and the numbering shifts. Named groups, written (?P<name>...), solve this by letting you retrieve each piece by name.

Named groups
pattern = r"Order #(?P<order_id>\d+) placed on (?P<order_date>\d{4}-\d{2}-\d{2})"
match = re.search(pattern, text)

print(match.group("order_id"))     # "4471"
print(match.group("order_date"))   # "2026-08-13"

# .groupdict() returns everything at once, as a regular dict
print(match.groupdict())
# {'order_id': '4471', 'order_date': '2026-08-13'}
🎯 Pro Tip
Prefer named groups over positional groups the moment a pattern has more than two capturing groups, or whenever the extracted data will be used further down the code — a call site that reads match.group("order_id") is self-documenting; one that reads match.group(3) forces the reader back up to the pattern to figure out what group 3 even is.
// Part 07 — Greedy vs Non-Greedy Quantifiers

Why * and + Grab More Than You Might Expect

By default, quantifiers are greedy — they match as much text as possible while still allowing the overall pattern to succeed. This is a genuinely common source of confusing bugs the first time it bites, especially with HTML-like or delimiter-heavy text.

The greedy trap
text = '<b>bold</b> and <i>italic</i>'

re.findall(r"<.+>", text)
# ['<b>bold</b> and <i>italic</i>']
# The greedy .+ matched from the FIRST < all the way to the LAST > —
# almost certainly not what was intended.

Adding a ? immediately after a quantifier makes it non-greedy (also called "lazy") — it matches as little text as possible instead.

The non-greedy fix
re.findall(r"<.+?>", text)
# ['<b>', '</b>', '<i>', '</i>']
# .+? stops at the FIRST > it can, giving four separate, correct matches
⚠️ Important
Regex is genuinely a weak tool for parsing real HTML or XML — nested tags and edge cases break it quickly, and this example exists purely to illustrate greedy vs non-greedy matching. For real HTML parsing, reach for a dedicated library such as BeautifulSoup, not re.
// Part 08 — Compiling Patterns

re.compile() — Reuse and Performance

Every call to a module-level function like re.search(pattern, text) compiles pattern into an internal matching engine representation before running it. Python caches recently-used compiled patterns automatically, so calling the same pattern repeatedly is not catastrophic — but when a pattern is used many times in a loop, or the same pattern is reused across a codebase, compiling it once explicitly with re.compile() is both faster and clearer about intent.

Compiling once, reusing many times
import re

ZIP_CODE = re.compile(r"^\d{5}(-\d{4})?$")   # optional +4 extension

candidates = ["80202", "80202-1234", "8020", "abc12"]
for c in candidates:
    if ZIP_CODE.match(c):
        print(f"{c}: valid")
    else:
        print(f"{c}: invalid")

A compiled pattern object exposes the same methods you have already used — .match(), .search(), .findall(), .sub(), .split() — just called directly on the compiled object instead of passing the pattern string to the module-level function each time.

🎯 Pro Tip
A practical convention worth adopting: give compiled patterns an ALL_CAPS name at module level, the same way you would a constant — it signals to any reader that this regex is a fixed, reusable definition, not something constructed fresh on every call.
// Part 09 — Raw Strings and Regex

Why Every Pattern in This Module Starts With r"..."

Back in Module 04, you met raw strings — r"..." — which tell Python not to interpret backslash escape sequences like \n or \t. Regex patterns use the backslash constantly for their own purposes (\d, \w, \s), and those meanings are completely unrelated to Python's own string escape sequences — which creates exactly the kind of collision raw strings exist to prevent.

What happens without a raw string
# WITHOUT r-prefix — Python's OWN string escaping interferes first
pattern = "\d+"     # Python sees \d, does not recognize it as a known escape,
                     # and (in modern Python) leaves it as the two characters \ and d —
                     # but this is fragile and inconsistent across escape sequences

# For example, \s is fine, but some sequences ARE meaningful to Python itself:
"\t"    # this is an actual TAB character to Python, not the two characters \ and t —
         # if you meant the regex whitespace escape \s and mistyped \t, you'd get a
         # literal tab character in your pattern instead of what you intended

# WITH r-prefix — completely unambiguous, exactly what you typed, character for character
pattern = r"\d+"    # r"\d+" is guaranteed to be backslash, d, plus — nothing else
⚠️ Important
Always write regex patterns as raw strings. It is not merely a convention — without the r prefix, certain digit/letter combinations after a backslash (like \b, which is a backspace character to Python but a word-boundary anchor in regex) produce silently wrong patterns instead of an error, which makes the bug genuinely hard to spot.
// Part 10 — Worked Example

Extracting Structured Data From Log Lines

Here is a realistic, complete example that pulls together everything above: parsing semi-structured application log lines into structured data, a task that comes up in nearly every backend engineering role.

Sample log lines
2026-08-13 09:14:02 ERROR [order-service] Failed to process order 4471: timeout after 30s
2026-08-13 09:14:07 INFO  [order-service] Order 4472 processed successfully
2026-08-13 09:15:33 ERROR [payment-service] Failed to process order 4473: card declined
Parsing every ERROR line into structured data
import re

LOG_PATTERN = re.compile(
    r"^(?P<timestamp>\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\s+"
    r"(?P<level>\w+)\s+"
    r"\[(?P<service>[\w-]+)\]\s+"
    r"(?P<message>.+)$"
)

log_lines = [
    "2026-08-13 09:14:02 ERROR [order-service] Failed to process order 4471: timeout after 30s",
    "2026-08-13 09:14:07 INFO  [order-service] Order 4472 processed successfully",
    "2026-08-13 09:15:33 ERROR [payment-service] Failed to process order 4473: card declined",
]

errors = []
for line in log_lines:
    match = LOG_PATTERN.match(line)
    if match and match.group("level") == "ERROR":
        order_match = re.search(r"order (\d+)", match.group("message"))
        errors.append({
            "timestamp": match.group("timestamp"),
            "service": match.group("service"),
            "order_id": order_match.group(1) if order_match else None,
            "message": match.group("message"),
        })

for e in errors:
    print(e)

# {'timestamp': '2026-08-13 09:14:02', 'service': 'order-service', 'order_id': '4471',
#  'message': 'Failed to process order 4471: timeout after 30s'}
# {'timestamp': '2026-08-13 09:15:33', 'service': 'payment-service', 'order_id': '4473',
#  'message': 'Failed to process order 4473: card declined'}

Notice how the pattern is built from exactly the pieces this module covered, composed together: named groups for the fields you need to extract, \d and \w shorthand classes, quantifiers for repeated digits, and a nested re.search() call to pull the order ID out of the already-extracted message text — a genuinely common pattern, where one regex extracts a broad structure and a second, narrower regex digs further into one piece of it.

// Part 11 — Real World
💼 What This Looks Like at Work

A Denver Insurance Platform's Silent Data-Quality Bug

Scenario — Insurance platform, Denver · Data quality incident

An insurance-quoting startup accepts phone numbers from a web form and needs to validate them before storing them, since downstream systems (SMS notifications, an agent dialer) assume a clean, consistent shape. An engineer writes a quick validation function under deadline pressure.

The original validation
import re

def is_valid_phone(number):
    return re.match(r"\d{3}-\d{3}-\d{4}", number) is not None

is_valid_phone("303-555-0192")          # True — correct
is_valid_phone("303-555-0192 ext 4")    # True — ALSO accepted, incorrectly

What breaks, three weeks later

The dialer system starts throwing errors on a small but growing fraction of stored numbers. The root cause, once found: exactly the missing-anchor bug from Part 05. re.match only pins the pattern to the start of the string, never the end, so any input that merely begins with a valid-looking phone number — with arbitrary trailing text — was silently accepted and stored as-is.

The fix, and the second issue it uncovers

The immediate fix is exactly Part 05's lesson: add a trailing $ anchor, r"^\d{3}-\d{3}-\d{4}$", so the entire string must match, not just a prefix of it. But the fix also surfaces a second, more interesting question in code review: should the validator reject (303) 555-0192 and 3035550192, both of which are genuinely valid phone numbers in a different but common format? The team ultimately normalizes input first — stripping non-digit characters with re.sub(r"\D", "", number) — and validates the normalized 10-digit result, rather than trying to write one pattern that accepts every real-world formatting style directly.

The lesson generalizes well beyond phone numbers: a regex validator that is not fully anchored on both ends does not fail loudly — it fails by silently accepting more than intended, which is exactly the kind of bug that survives testing and only shows up once real, messy user input reaches it in production.

// Part 12 — Misconceptions

Four Misconceptions About Regular Expressions

✕ ""Regex is always the fastest and most correct way to check text""
For prefix, suffix, and substring checks, plain string methods (str.startswith, str.endswith, in) are simpler, faster, and easier for the next reader to understand at a glance. Reach for regex specifically when the pattern has real structure — repetition, optional parts, or alternation — that string methods cannot express.
✕ ""re.match checks the whole string, like a full validation""
re.match only anchors the pattern to the START of the string, never the end. A pattern with no trailing $ will match a string that starts correctly but has arbitrary extra content after it — exactly the bug in the Real World example above. Use ^pattern$ for genuine full-string validation.
✕ ""Quantifiers like + and * always match the shortest possible piece of text""
By default, quantifiers are greedy — they match as MUCH text as possible while still letting the pattern succeed overall, which can grab far more than intended across multiple delimiters. Add a ? after the quantifier (+? or *?) to make it non-greedy, matching as little as possible instead.
✕ ""Regex can parse any structured text format, including HTML and XML""
Regex works well on flat, line-oriented, or simply-delimited text, but genuinely struggles with deeply nested structures like HTML and XML, where matching pairs of tags correctly requires more than pattern matching can express. Use a dedicated parser (like BeautifulSoup for HTML, or the built-in xml module) for those formats instead.
// Part 13 — Interview Prep

5 Interview Questions — With Complete Answers

What is the difference between re.match, re.search, and re.findall?
re.match only checks whether the pattern matches at the very START of the string, and returns a single Match object or None. re.search checks the entire string for the first occurrence of the pattern, anywhere, also returning a single Match object or None. re.findall returns every non-overlapping match in the string as a plain list of strings (or tuples, if the pattern has multiple groups), rather than a Match object.
Why should regex patterns in Python always be written as raw strings?
Regex uses the backslash for its own escape sequences (\d, \w, \s, \b, and more), which are unrelated to Python's own string escape sequences. Without the r prefix, Python's own string parser processes backslash sequences first — and some combinations (like \b, a backspace character to Python but a word-boundary anchor in regex) silently produce a different pattern than intended, rather than raising an error. The r prefix guarantees the pattern is passed to re exactly as typed.
What is the difference between a greedy and a non-greedy quantifier?
By default, quantifiers (*, +, {n,m}) are greedy — they match as much text as possible while still allowing the overall pattern to succeed. Adding a ? immediately after the quantifier (*?, +?) makes it non-greedy (lazy), matching as little text as possible instead. This matters most when a pattern could span multiple occurrences of a delimiter, like matching content between HTML-like tags.
What is a capturing group, and how do named groups improve on plain numbered groups?
A capturing group, written with parentheses (...), marks a piece of the overall pattern that can be extracted individually from a Match object via .group(n), rather than only getting the full match back as one block. Named groups, written (?P<name>...), let you retrieve the same piece by a descriptive name via .group("name") instead of counting parentheses — this stays correct and readable even after the pattern is edited and the numeric positions shift.
Why compile a regex pattern with re.compile() instead of just calling re.search(pattern, text) each time?
Compiling once, especially for a pattern used repeatedly (inside a loop, or reused across a codebase), avoids recompiling the same pattern on every call and makes the code's intent clearer — a module-level compiled pattern with an ALL_CAPS name reads as a defined, reusable constant. The compiled object exposes the same methods (.match, .search, .findall, .sub, .split) called directly on it.
// Common Mistakes

Regex Mistakes Beginners Make Constantly

Forgetting the $ anchor when a pattern is meant to validate a whole string
re.match(r"^\d{5}", value) only confirms the string STARTS with five digits — anything can follow. Add a trailing $ for genuine full-string validation, as covered in Part 05 and the Real World example.
Forgetting the r prefix on a pattern string
Without it, Python's own string escaping can silently alter certain backslash sequences before re ever sees them. Always write patterns as raw strings: r"\d+", not "\d+".
Calling .group() on a match that might be None
re.search() and re.match() return None when nothing matches. Calling .group() directly on that result raises AttributeError: 'NoneType' object has no attribute 'group'. Always check the result first: match = re.search(...); if match: ...
Using .findall() when the pattern has multiple groups and being surprised by the return shape
If a pattern has more than one capturing group, findall returns a list of TUPLES (one tuple per match, containing each group), not a flat list of full matches. Print a small example first if you are unsure what shape to expect.
Writing an overly greedy pattern and matching far more than intended
A pattern like r"<.+>" against text with multiple tags matches from the very first < to the very last > in the whole string. Use the non-greedy r"<.+?>" when the intent is the shortest reasonable match, as shown in Part 07.
// Error Library

Errors You Will Hit With Regex — And Exactly Why

re.error: missing ), unterminated subpattern at position 4
Cause: An opening parenthesis in the pattern has no matching closing parenthesis — a common typo when writing or editing a capturing group.
Fix: Count the parentheses in the pattern carefully, or build complex patterns incrementally, testing each addition with a quick re.search() call before adding the next piece.
AttributeError: 'NoneType' object has no attribute 'group'
Cause: re.search() or re.match() returned None because the pattern did not match anywhere in the string, and .group() was called on that None result directly without checking first.
Fix: Always store the result and check it before calling .group(): match = re.search(pattern, text); if match: match.group(1).
re.error: bad escape \d at position 0
Cause: This specific message is rare in Python 3 (\d is a valid, recognized escape), but similar errors appear when a pattern uses an escape sequence re does not recognize, or when a raw string was not used and Python's own string processing mangled the backslash sequence before re received it.
Fix: Confirm the pattern is written as a raw string (r"...") and double-check the escape sequence against the standard re shorthand classes (\d, \w, \s and their negations).
IndexError: no such group
Cause: Calling .group(n) with a group number that does not exist in the pattern — for example, .group(2) when the pattern only has one set of parentheses.
Fix: Recount the capturing groups in the pattern, or switch to named groups (?P<name>...) so retrieval does not depend on getting the numbering exactly right.
A pattern "works" in testing but matches unexpected substrings in production
Cause: Almost always a missing anchor (^ and/or $) — the pattern was written to check for a shape ANYWHERE in the string, but was actually intended as full-string validation.
Fix: For validation use cases specifically, anchor both ends: r"^pattern$". Test the fixed pattern against both valid input and input with unexpected trailing/leading content.

🎯 Key Takeaways

  • Regex is for patterns with real structure — repetition, optional parts, alternation. For simple prefix/suffix/substring checks, plain string methods are simpler and faster.
  • re.match anchors to the START of the string only; re.search finds the first match anywhere; re.findall returns every match as a list. Know which one you actually need.
  • Character classes ([], \d, \w, \s) match one character from a set. Quantifiers (*, +, ?, {n,m}) say how many times the preceding element repeats.
  • Anchors ^ and $ pin a match to the start and end of the string. Full-string validation requires BOTH — a missing $ is one of the most common real regex bugs.
  • Capturing groups (...) extract pieces of a match individually via .group(n). Named groups (?P<name>...) do the same by name, and stay correct even after the pattern is edited.
  • Quantifiers are greedy by default, matching as much as possible. Add ? after a quantifier (+?, *?) for non-greedy matching.
  • re.compile() compiles a pattern once for reuse — clearer and faster than repeatedly calling a module-level re function with the same pattern string.
  • Always write regex patterns as raw strings (r"...") — Python's own string escaping can otherwise silently corrupt certain backslash sequences before re ever sees them.
  • Regex is a weak tool for deeply nested formats like HTML and XML — use a dedicated parser for those instead.

What comes next

Module 33 covers dates and times — the datetime module, timezone-aware datetimes, and the formatting codes that trip up almost everyone the first time they need them.

Module 33 → Working with Dates and Times
Share

Discussion

0

Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.

Continue with GitHub
Loading...