Modules and the standard libraryChapter 75 of 114
RegEx
Find and replace text by pattern rather than by exact match.
The re module
A regular expression describes a shape of text. re matches those shapes:
import re
text = "call 0176 1234567 or 0151 7654321"
print(re.findall(r"\d+", text))Output
['0176', '1234567', '0151', '7654321']
Always write patterns as raw strings. Without the r, Python processes the backslashes before re ever sees them.
The pieces worth knowing
| Pattern | Matches |
|---|---|
\d | a digit |
\w | a letter, digit or underscore |
\s | whitespace |
. | any character except newline |
[abc] | one of a, b or c |
[^abc] | anything except those |
+ | one or more |
* | zero or more |
? | zero or one |
{2,4} | between two and four |
^ / $ | start / end of the string |
\b | a word boundary |
import re
print(re.findall(r"\b\w{5}\b", "the quick brown fox jumps"))
print(re.findall(r"[aeiou]", "programming"))
print(re.findall(r"^\w+", "first word only"))Output
['quick', 'brown', 'jumps'] ['o', 'a', 'i'] ['first']
The main functions
import re
text = "Ada was born in 1815"
print(re.search(r"\d{4}", text).group())
print(re.findall(r"\w+", text))
print(re.sub(r"\d{4}", "YYYY", text))
print(re.split(r"\s+", text))Output
1815 ['Ada', 'was', 'born', 'in', '1815'] Ada was born in YYYY ['Ada', 'was', 'born', 'in', '1815']
searchfinds the first match anywhere, orNonematchonly matches at the startfindallgives every match as a listsubreplacessplitbreaks on the pattern
search returns None when there is nothing
import re
found = re.search(r"\d+", "no digits here")
print(found)
if found:
print(found.group())
else:
print("nothing matched")Output
None nothing matched
Calling .group() on that None raises AttributeError, which is the most common regex mistake. Check first.
Capture groups
Brackets capture part of the match:
import re
match = re.search(r"(\d{2})/(\d{2})/(\d{4})", "dated 14/03/2026 exactly")
print(match.group(0))
print(match.group(1))
print(match.groups())Output
14/03/2026
14
('14', '03', '2026')Group 0 is the whole match; the rest are the brackets, left to right.
Named groups read far better once there are more than two:
import re
pattern = r"(?P<day>\d{2})/(?P<month>\d{2})/(?P<year>\d{4})"
match = re.search(pattern, "14/03/2026")
print(match.group("year"))
print(match.groupdict())Output
2026
{'day': '14', 'month': '03', 'year': '2026'}With groups, findall returns the groups rather than the whole match:
import re
print(re.findall(r"(\w+)@(\w+)\.com", "a@x.com b@y.com"))Output
[('a', 'x'), ('b', 'y')]Greedy by default
+ and * take as much as they can. Add ? to take as little:
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>']
The greedy version ran from the first < to the last >. This surprises everyone once.
Flags
import re
print(re.findall(r"ada", "Ada and ADA", re.IGNORECASE))
print(re.sub(r"\s+", " ", "too much\n\nspace"))Output
['Ada', 'ADA'] too much space
Compile a pattern you reuse
import re
word = re.compile(r"\b[A-Z]\w+")
print(word.findall("Ada met Grace in London"))
print(word.sub("X", "Ada met Grace"))Output
['Ada', 'Grace', 'London'] X met X
When not to use it
A regex is powerful and hard to read. If a string method does the job, use it:
name = "report.pdf"
print(name.endswith(".pdf"))
print(name.split("."))Output
True ['report', 'pdf']
Test yourself
3 questionsWhy should regex patterns be raw strings?
Show the answer
Otherwise Python processes the backslashes before re sees them — Without the r you end up escaping every backslash twice.
What does re.search return when nothing matches?
Show the answer
None — Calling .group() on that None is the most common regex mistake. Check the result first.
Why does <.+> match the whole string rather than one tag?
Show the answer
+ is greedy and takes as much as it can — Add a ? to make it lazy: <.+?> matches each tag separately.
itertools
Building blocks for looping, that never build the whole sequence.