PythonRegular Expressions (re)

Regular Expressions (re)

Regular expressions ("regex") describe patterns of text — validating an email address, extracting a date buried in a log line, splitting on multiple kinds of whitespace, or rewriting text that follows a shape rather than an exact string. Python exposes them through the built-in re module. Regex syntax itself is not Python-specific — the same patterns work (with minor dialect differences) in JavaScript, Java, grep, and most other languages.

Basic Pattern Syntax

A regex pattern is built from literal characters (which match themselves) and special metacharacters that match categories or repetitions of characters.

Syntax

Meaning

.

Any single character (except newline by default)

*

Zero or more of the preceding item

+

One or more of the preceding item

?

Zero or one of the preceding item

\d

A digit (0-9)

\w

A word character (letters, digits, underscore)

\s

A whitespace character (space, tab, newline)

^

Start of the string (or line, in multiline mode)

$

End of the string (or line, in multiline mode)

[]

A character class, e.g. [aeiou] matches any one vowel

Warning
Regex patterns should almost always be written as raw strings: `r"\d+"`, not `"\d+"`. Without the `r` prefix, Python's own string parser tries to interpret backslash sequences first — `\d` isn't a recognized Python escape so it currently passes through unchanged (with a `DeprecationWarning`), but sequences like `\n` or `\t` inside a non-raw pattern string get converted to an actual newline or tab *before* `re` ever sees the pattern, silently changing what you meant to match. Using `r"..."` avoids this entire class of bug.
`match()` vs `search()` vs `findall()`

These three functions are the most common entry points into re, and mixing them up is a frequent source of bugs.

Function

Where it looks

Returns

re.match(pattern, text)

Only at the very start of the string

A Match object, or None if the start doesn't match

re.search(pattern, text)

Anywhere in the string (first match)

A Match object, or None if not found

re.findall(pattern, text)

Anywhere in the string, all non-overlapping matches

A list of matched strings (or tuples of groups)

Python
import re

text = "Order #4471 shipped, invoice #4472 pending"

print(re.match(r"\d+", text))          # None: string doesn't start with a digit
print(re.search(r"\d+", text))         # matches the first number found anywhere
print(re.findall(r"\d+", text))        # every number in the string
None
<re.Match object; span=(7, 11), match='4471'>
['4471', '4472']
Groups: Capturing Parts of a Match

Parentheses () mark a group within a pattern. A successful Match object exposes each group through .group(n).group(0) (or just .group()) is the whole match, and .group(1), .group(2), etc. are the parenthesized sub-matches, in order.

Python
import re

log_line = "2026-07-06 14:30:00 ERROR Connection refused"
m = re.search(r"(\d{4}-\d{2}-\d{2}) (\d{2}:\d{2}:\d{2}) (\w+)", log_line)

if m:
    print("Full match:", m.group(0))
    print("Date:", m.group(1))
    print("Time:", m.group(2))
    print("Level:", m.group(3))
Full match: 2026-07-06 14:30:00 ERROR
Date: 2026-07-06
Time: 14:30:00
Level: ERROR

Numbered groups work, but they get hard to read once a pattern has more than two or three of them, and inserting a new group shifts every number after it. Named groups — (?P<name>...) — solve both problems by letting you access captures via .group("name") instead of a position.

Python
import re

log_line = "2026-07-06 14:30:00 ERROR Connection refused"
pattern = r"(?P<date>\d{4}-\d{2}-\d{2}) (?P<time>\d{2}:\d{2}:\d{2}) (?P<level>\w+)"
m = re.search(pattern, log_line)

if m:
    print(m.group("date"))
    print(m.group("time"))
    print(m.group("level"))
    print(m.groupdict())
2026-07-06
14:30:00
ERROR
{'date': '2026-07-06', 'time': '14:30:00', 'level': 'ERROR'}
Find-and-Replace with `re.sub()`

re.sub(pattern, replacement, text) returns a new string with every match of pattern replaced by replacement. The replacement string can reference captured groups with backreferences (\\1, \\2, ...), letting you rearrange matched text rather than just deleting or replacing it outright.

Python
import re

dates = "Meeting on 07/06/2026 and follow-up on 07/09/2026"

# Rewrite MM/DD/YYYY as YYYY-MM-DD using group backreferences
iso_dates = re.sub(r"(\d{2})/(\d{2})/(\d{4})", r"\3-\1-\2", dates)
print(iso_dates)
Meeting on 2026-07-06 and follow-up on 2026-07-09
Tip
`re.sub()` also accepts a function as the replacement argument instead of a string. The function receives the `Match` object and returns the string to substitute — useful when the replacement needs logic, not just rearrangement (e.g. uppercasing matched text).
Compiling Patterns with `re.compile()`

Every top-level function like re.search(pattern, text) compiles pattern into an internal representation before matching. If you're applying the same pattern repeatedly — inside a loop over thousands of lines, for example — compiling it once with re.compile() and reusing the resulting pattern object avoids repeating that compilation work and reads more clearly at the call site.

Python
import re

ip_pattern = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")

log_lines = [
    "connection from 192.168.1.10 accepted",
    "connection from 10.0.0.5 rejected",
    "heartbeat ok",
]

for line in log_lines:
    match = ip_pattern.search(line)
    if match:
        print(f"Found IP {match.group()} in: {line}")
Found IP 192.168.1.10 in: connection from 192.168.1.10 accepted
Found IP 10.0.0.5 in: connection from 10.0.0.5 rejected
Note
A compiled pattern object (returned by `re.compile()`) has the same methods — `.match()`, `.search()`, `.findall()`, `.sub()` — just called on the pattern itself instead of on the `re` module, with the pattern argument no longer needed.
  • Always write regex patterns as raw strings: r"...".

  • match() anchors to the start of the string; search() looks anywhere; findall() returns every match as a list.

  • Use (...) for numbered groups and (?P<name>...) for named groups, accessed via .group(n) or .group("name").

  • re.sub() supports backreferences (\1, \2, ...) in its replacement string to reuse captured text.

  • Compile with re.compile() when reusing the same pattern many times.