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
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))
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
| Pattern | Matches |
|---|---|
. | Any character except newline |
\d \w \s | Digit, word character (letter/digit/_), whitespace |
\D \W \S | The 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 |
^ $ \b | Start, end, word boundary |
a|b, ( ) | Alternatives, groups |
match, fullmatch and search
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)), )
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
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())
['#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
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())
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
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"))
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
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())
['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
import re html = "<b>bold</b> and <i>italic</i>" print(re.findall(r"<.+>", html)) print(re.findall(r"<.+?>", html))
['<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:
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")))
C\+\+\ \(2026\) True