Regular Expressions

A regular expression is a pattern for matching text. Python's re module can validate formats, find every email address in a document, pull structured fields out of log lines, or perform sophisticated search-and-replace — usually in a single line.

Searching for a pattern

PY search.py
import re

text = "Order 1042 shipped on 2026-10-06 to Lisbon"

match = re.search(r"\d+", text)
print(match.group(), match.start(), match.end())

print(re.search(r"\d{4}-\d{2}-\d{2}", text).group())
print(re.search(r"Paris", text))
Output
1042 6 10
2026-10-06
None

Always write patterns as raw strings (r"...") so backslashes reach the regex engine untouched. re.search returns a match object for the first match, or None.

Pattern building blocks

PatternMatches
.Any character except newline
\d \w \sDigit, word character (letter/digit/_), whitespace
\D \W \SThe opposites
[abc], [a-z0-9], [^,]One character from a set; ^ negates
* + ?0 or more, 1 or more, 0 or 1
{3} {2,5}Exact count, range
^ $ \bStart, end, word boundary
a|b, ( )Alternatives, groups

match, fullmatch and search

PY fullmatch.py
import re

postcode = re.compile(r"\d{4}-\d{3}")

for value in ["1100-148", "1100-148x", "abc 1100-148"]:
    print(
        value.ljust(13),
        bool(postcode.search(value)),
        bool(postcode.match(value)),
        bool(postcode.fullmatch(value)),
    )
Output
1100-148      True True True
1100-148x     True True False
abc 1100-148  True False False

search finds the pattern anywhere; match only at the start; fullmatch requires the whole string to match — the right choice for validation. re.compile creates a reusable pattern object, which is tidy when a pattern is used repeatedly.

Finding all matches

PY findall.py
import re

post = "Loving #python and #regex! Follow @ana_dev and @ben for more #python tips"

print(re.findall(r"#\w+", post))
print(sorted(set(re.findall(r"#(\w+)", post))))
print(re.findall(r"@(\w+)", post))

for m in re.finditer(r"#\w+", post):
    print(m.group(), "at", m.start())
Output
['#python', '#regex', '#python']
['python', 'regex']
['ana_dev', 'ben']
#python at 7
#regex at 19
#python at 61

If the pattern contains a group, findall returns just the group's contents. finditer yields full match objects, with positions.

Groups and named groups

PY groups.py
import re

line = "2026-10-06 09:15:02 ERROR [db] connection timeout after 30s"
pattern = re.compile(
    r"(?P<date>\S+) (?P<time>\S+) (?P<level>[A-Z]+) \[(?P<source>\w+)\] (?P<message>.*)"
)

m = pattern.match(line)
print(m.group("level"), m.group("source"))
print(m.groupdict())
Output
ERROR db
{'date': '2026-10-06', 'time': '09:15:02', 'level': 'ERROR', 'source': 'db', 'message': 'connection timeout after 30s'}

Named groups, (?P<name>...), make the extracted fields self-documenting. groupdict() turns a whole log line into a dictionary.

Replacing with sub

PY sub.py
import re

print(re.sub(r"\s+", " ", "too    many   spaces"))
print(re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\3/\2/\1", "Due 2026-10-06"))
print(re.sub(r"\d", "#", "Card 4111 1111 1111 1234", count=12))

def to_upper(m):
    return m.group(1).upper()

print(re.sub(r"_(\w)", to_upper, "user_first_name"))
Output
too many spaces
Due 06/10/2026
Card #### #### #### 1234
userFirstName

In the replacement, \1, \2… refer to groups. A function can compute each replacement — here converting snake_case to camelCase.

Splitting and flags

PY split_flags.py
import re

print(re.split(r"[,;\s]+", "apples, pears;plums  kiwis"))
print(re.findall(r"python", "Python PYTHON python", flags=re.IGNORECASE))

text = """name: Ana
role: admin
name: Ben"""
print(re.findall(r"^name: (\w+)$", text, flags=re.MULTILINE))

phone = re.compile(r"""
    \(?(\d{3})\)?   # area code, optional parentheses
    [\s.-]?         # separator
    (\d{3})[\s.-]?(\d{4})
""", re.VERBOSE)
print(phone.search("Call (555) 123-4567 today").groups())
Output
['apples', 'pears', 'plums', 'kiwis']
['Python', 'PYTHON', 'python']
['Ana', 'Ben']
('555', '123', '4567')

re.IGNORECASE, re.MULTILINE (make ^/$ work per line) and re.VERBOSE (allow whitespace and comments in the pattern) are the flags you'll use most.

Greedy vs lazy

PY greedy.py
import re

html = "<b>bold</b> and <i>italic</i>"
print(re.findall(r"<.+>", html))
print(re.findall(r"<.+?>", html))
Output
['<b>bold</b> and <i>italic</i>']
['<b>', '</b>', '<i>', '</i>']

Quantifiers are greedy by default — they match as much as possible. Adding ? makes them lazy. (For real HTML, use a parser such as html.parser or BeautifulSoup rather than regexes.)

Escaping user input

Characters like . * + ? ( ) [ ] $ ^ have special meanings. When a pattern includes text from a user, escape it first:

PY escape.py
import re

search_term = "C++ (2026)"
print(re.escape(search_term))
print(bool(re.search(re.escape(search_term), "I'm learning C++ (2026) now")))
Output
C\+\+\ \(2026\)
True

Try it yourself

Try it yourself
Console output