Regular expressions in Python: a practical guide with the traps

Regular expressions find and reshape text. Learn groups, greedy versus lazy, a Bangladeshi mobile number check and the backtracking trap that can freeze a server.

A line of text with the matched part bracketed and highlighted under a magnifier

A regular expression (regex) describes a pattern of text, so you can find, check or rewrite it. It is excellent for logs, simple validation and clean-up, and easy to misuse for the things it cannot do well. The code and output below were run with Python 3 before publishing.

The building blocks

  • \d a digit, \w a letter, digit or underscore, \s whitespace, . any character except a newline.
  • + one or more, * zero or more, ? optional, {4} exactly four.
  • [A-Z] a character from a set, ^ start, $ end.
  • ( ... ) a group, (?P<name> ... ) a named group.
  • Write patterns as raw strings (r"...") so the backslashes reach the regex engine.

A tested tour

import re

log = "2026-10-29 ERROR payment failed order=1042 user=amina@example.com"
m = re.search(r"(?P<date>\d{4}-\d{2}-\d{2}) (?P<level>[A-Z]+) (?P<msg>.*?) order=(?P<order>\d+)", log)
print(m.groupdict())

print(re.findall(r"[\w.+-]+@[\w-]+\.[\w.]+", "mail amina@example.com or ravi@mail.test today"))

html = "<b>one</b> and <b>two</b>"
print("greedy:", re.findall(r"<b>.*</b>", html), "| lazy:", re.findall(r"<b>.*?</b>", html))

print(re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\3/\2/\1", "due 2026-11-03"))

phone = re.compile(r"^(?:\+?88)?01[3-9]\d{8}$")
print([bool(phone.match(x)) for x in ["01712345678", "+8801712345678", "8801712345678", "0171234567", "02112345678"]])
{'date': '2026-10-29', 'level': 'ERROR', 'msg': 'payment failed', 'order': '1042'}
['amina@example.com', 'ravi@mail.test']
greedy: ['<b>one</b> and <b>two</b>'] | lazy: ['<b>one</b>', '<b>two</b>']
due 03/11/2026
[True, True, True, False, False]

Things to notice:

  • Named groups turn a log line into a dictionary you can read by name.
  • Greedy vs lazy. .* grabs as much as possible and swallowed both bold tags, while .*? stops at the first </b>.
  • re.sub reuses groups (\3/\2/\1) to reorder a date.
  • The phone check accepts 017..., +88017... and 88017..., and rejects a number with too few digits. It is an example for a common Bangladeshi mobile format, not a complete rule for every valid number.

The trap: catastrophic backtracking

Some patterns take exponentially longer on input that almost matches. This is how a single request can freeze a server (ReDoS).

import re, time

def run(pattern, n):
    s = "a" * n + "b"
    t = time.perf_counter(); re.match(pattern, s); return time.perf_counter() - t

for n in (20, 22, 24):
    print(f"(a+)+$ on {n} a's then b: {run(r'(a+)+$', n):.3f} s | a+$: {run(r'a+$', n):.6f} s")
(a+)+$ on 20 a's then b: 0.031 s | a+$: 0.000057 s
(a+)+$ on 22 a's then b: 0.121 s | a+$: 0.000006 s
(a+)+$ on 24 a's then b: 0.473 s | a+$: 0.000008 s

The nested quantifier (a+)+ roughly quadrupled each time we added two letters, while the safe a+$ stayed instant. On a longer string this becomes minutes.

How to stay safe

  • Avoid nested quantifiers like (a+)+, (.*)* and (a|aa)+.
  • Cap the input length before matching user text.
  • Prefer simple tools when they are enough: str.startswith, split, in, or a real parser for HTML and JSON.
  • For email, do a light check and send a confirmation message. A regex cannot prove an address works.
  • Compile once with re.compile if you use a pattern many times.

Common mistakes

  • Using regex to parse HTML. Use a parser.
  • Forgetting the raw string prefix and doubling backslashes by accident.
  • Using re.match when you meant re.search (match only checks the start).
  • Forgetting anchors, so \d{4} matches inside a longer number.
  • Writing one huge pattern instead of splitting the work into steps.

Main idea: use named groups, prefer lazy quantifiers where you want the shortest match, anchor your patterns, and never run a pattern with nested quantifiers on untrusted input.


Programming roadmaps series, Part 3 (writing better Python): 2 of 5. Previous: Python virtual environments and packaging: a clean setup.

Keep reading