Regular Expressions
1 · The lesson
readA regex (regular expression) is a small pattern language for describing the shape of a piece of text. "An email address." "A date in YYYY-MM-DD form." "Three digits followed by a dash." You write the pattern, and the regex engine finds the text that matches it (or reports that nothing matches).
Regex is known for being hard to read later, and that is often true when people use it for the wrong job. This lesson teaches the parts you will really use, when to use them, and, just as important, when to stop using regex and use a real parser instead.
1. The re Module: Your First Pattern
import re text = "Order 4271 shipped on 2026-03-14" m = re.search(r"\d+", text) # find the first run of digits print(m) # <re.Match object; span=(6, 10), match='4271'> print(m.group()) # '4271' print(m.span()) # (6, 10) (the start and end index)
Note three things in this example:
1. The r"..." raw string prefix. Always. Regex uses \ a lot (\d, \b, \w). Without r, Python's string parser processes the backslashes first, and you get bugs or warnings. We'll come back to this in Section 10.
2. re.search finds the first match anywhere in the string. It returns a Match object or None.
3. Match.group() gives you the matched text. Match.span() gives you the position.
2. The Five Functions You'll Use Most
import re text = "alice@example.com, bob@example.com, carol@other.org" # 1. search: the first match anywhere, or None m = re.search(r"\w+@\w+\.\w+", text) print(m.group()) # alice@example.com # 2. match: a match only at the START of the string print(re.match(r"bob@", text)) # None (match only checks at index 0, and text starts with "alice@") print(re.match(r"alice", text)) # Match object # 3. findall: every match, as a list of strings print(re.findall(r"\w+@\w+\.\w+", text)) # ['alice@example.com', 'bob@example.com', 'carol@other.org'] # 4. finditer: every match, as Match objects (best when you need positions) for m in re.finditer(r"\w+@\w+\.\w+", text): print(m.group(), "at", m.span()) # 5. sub: find and replace print(re.sub(r"\w+@\w+\.\w+", "[EMAIL]", text)) # [EMAIL], [EMAIL], [EMAIL]
Beginners often confuse search and match: match only matches at the start of the string, while search looks anywhere. When in doubt, use search. match is mainly useful when you're parsing a known format from position 0 (like a line in a structured log).
findall is convenient but gives you only strings, with no positions. If the pattern has groups, it returns the groups (as tuples when there are several), not the full match. When you need more than findall gives you, use finditer.
3. Character Classes
Character classes answer the question "what kind of character can be here?"
| Pattern | Means |
|---|---|
\d | A digit. Equivalent to [0-9] for ASCII. |
\D | A non-digit. |
\w | A "word" character: letter, digit, or underscore. |
\W | A non-word character. |
\s | Whitespace: space, tab, newline. |
\S | Non-whitespace. |
. | Any character except newline (unless re.DOTALL is set). |
[abc] | Any one of a, b, or c. |
[^abc] | Any character that is NOT a, b, or c. |
[a-z] | Any lowercase letter. Ranges work in sets. |
[A-Za-z0-9_] | \w written by hand (ASCII only). Useful when you want to be explicit. |
re.findall(r"\d", "abc123") # ['1', '2', '3'] re.findall(r"\d+", "abc123 def456") # ['123', '456'] re.findall(r"[aeiou]", "regex lesson") # ['e', 'e', 'e', 'o'] re.findall(r"[^aeiou\s]", "regex") # ['r', 'g', 'x'] (the consonants)
setup added so this can run · re-runs 2 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) re = _AutoMock('re') # Earlier blocks on this page that this one builds on, re-run quietly first. try: import re except Exception: pass _pm_earlier = [ 'import re\n\ntext = "Order 4271 shipped on 2026-03-14"\n\nm = re.search(r"\\d+", text) # find the first run of digits\nprint(m) # <re.Match object; span=(6, 10), match=\'4271\'>\nprint(m.group()) # \'4271\'\nprint(m.span()) # (6, 10) (the start and end index)', 'import re\n\ntext = "alice@example.com, bob@example.com, carol@other.org"\n\n# 1. search: the first match anywhere, or None\nm = re.search(r"\\w+@\\w+\\.\\w+", text)\nprint(m.group()) # alice@example.com\n\n# 2. match: a match only at the START of the string\nprint(re.match(r"bob@", text)) # None (match only checks at index 0, and text starts with "alice@")\nprint(re.match(r"alice", text)) # Match object\n\n# 3. findall: every match, as a list of strings\nprint(re.findall(r"\\w+@\\w+\\.\\w+", text))\n# [\'alice@example.com\', \'bob@example.com\', \'carol@other.org\']\n\n# 4. finditer: every match, as Match objects (best when you need positions)\nfor m in re.finditer(r"\\w+@\\w+\\.\\w+", text):\n print(m.group(), "at", m.span())\n\n# 5. sub: find and replace\nprint(re.sub(r"\\w+@\\w+\\.\\w+", "[EMAIL]", text))\n# [EMAIL], [EMAIL], [EMAIL]', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
Inside [], most metacharacters lose their special meaning: [.] is a literal dot, [+] is a literal plus. The exceptions are ^ (negation, only at start), - (range), ] (closes the class), and \ (escape).
4. Quantifiers: How Many?
| Pattern | Means |
|---|---|
? | 0 or 1 (optional) |
* | 0 or more |
+ | 1 or more |
{n} | Exactly n |
{n,m} | Between n and m |
{n,} | At least n |
All of these are greedy by default: they match as much text as possible, and then give some back (backtrack) if the rest of the pattern needs it. Add ? after them to make them lazy (also called non-greedy or reluctant).
text = "<b>bold</b> and <i>italic</i>" re.findall(r"<.*>", text) # ['<b>bold</b> and <i>italic</i>'] (greedy) re.findall(r"<.*?>", text) # ['<b>', '</b>', '<i>', '</i>'] (lazy)
setup added so this can run · re-runs 3 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) re = _AutoMock('re') # Earlier blocks on this page that this one builds on, re-run quietly first. try: import re except Exception: pass _pm_earlier = [ 'import re\n\ntext = "Order 4271 shipped on 2026-03-14"\n\nm = re.search(r"\\d+", text) # find the first run of digits\nprint(m) # <re.Match object; span=(6, 10), match=\'4271\'>\nprint(m.group()) # \'4271\'\nprint(m.span()) # (6, 10) (the start and end index)', 'import re\n\ntext = "alice@example.com, bob@example.com, carol@other.org"\n\n# 1. search: the first match anywhere, or None\nm = re.search(r"\\w+@\\w+\\.\\w+", text)\nprint(m.group()) # alice@example.com\n\n# 2. match: a match only at the START of the string\nprint(re.match(r"bob@", text)) # None (match only checks at index 0, and text starts with "alice@")\nprint(re.match(r"alice", text)) # Match object\n\n# 3. findall: every match, as a list of strings\nprint(re.findall(r"\\w+@\\w+\\.\\w+", text))\n# [\'alice@example.com\', \'bob@example.com\', \'carol@other.org\']\n\n# 4. finditer: every match, as Match objects (best when you need positions)\nfor m in re.finditer(r"\\w+@\\w+\\.\\w+", text):\n print(m.group(), "at", m.span())\n\n# 5. sub: find and replace\nprint(re.sub(r"\\w+@\\w+\\.\\w+", "[EMAIL]", text))\n# [EMAIL], [EMAIL], [EMAIL]', 're.findall(r"\\d", "abc123") # [\'1\', \'2\', \'3\']\nre.findall(r"\\d+", "abc123 def456") # [\'123\', \'456\']\nre.findall(r"[aeiou]", "regex lesson") # [\'e\', \'e\', \'e\', \'o\']\nre.findall(r"[^aeiou\\s]", "regex") # [\'r\', \'g\', \'x\'] (the consonants)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
The greedy version starts at the first <. Then .* matches everything to the end of the string, including the other angle brackets, and backtracks until it finds the last >. The lazy version stops at the first > after each <. Both are valid, but for tag-like delimiters you almost always want lazy.
(But if you are really parsing HTML, read Section 11 first: don't use regex for it.)
5. Anchors: Where in the String?
| Pattern | Means |
|---|---|
^ | Start of string (or start of line with re.MULTILINE) |
$ | End of string (or end of line with re.MULTILINE) |
\b | Word boundary: the position between a \w and a \W character |
\B | Non-word-boundary |
\b is the one people forget most often. It matches the position between a word character and a non-word character (or the start or end of the string). It matches no characters itself. Use it for "match the word cat but not catalogue":
text = "cat caterpillar scattered cats" re.findall(r"cat", text) # ['cat', 'cat', 'cat', 'cat'] (too many) re.findall(r"\bcat\b", text) # ['cat'] (only the whole word) re.findall(r"\bcat", text) # ['cat', 'cat', 'cat'] (cat at the start of a word)
setup added so this can run · re-runs 4 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) re = _AutoMock('re') # Earlier blocks on this page that this one builds on, re-run quietly first. try: import re except Exception: pass _pm_earlier = [ 'import re\n\ntext = "Order 4271 shipped on 2026-03-14"\n\nm = re.search(r"\\d+", text) # find the first run of digits\nprint(m) # <re.Match object; span=(6, 10), match=\'4271\'>\nprint(m.group()) # \'4271\'\nprint(m.span()) # (6, 10) (the start and end index)', 'import re\n\ntext = "alice@example.com, bob@example.com, carol@other.org"\n\n# 1. search: the first match anywhere, or None\nm = re.search(r"\\w+@\\w+\\.\\w+", text)\nprint(m.group()) # alice@example.com\n\n# 2. match: a match only at the START of the string\nprint(re.match(r"bob@", text)) # None (match only checks at index 0, and text starts with "alice@")\nprint(re.match(r"alice", text)) # Match object\n\n# 3. findall: every match, as a list of strings\nprint(re.findall(r"\\w+@\\w+\\.\\w+", text))\n# [\'alice@example.com\', \'bob@example.com\', \'carol@other.org\']\n\n# 4. finditer: every match, as Match objects (best when you need positions)\nfor m in re.finditer(r"\\w+@\\w+\\.\\w+", text):\n print(m.group(), "at", m.span())\n\n# 5. sub: find and replace\nprint(re.sub(r"\\w+@\\w+\\.\\w+", "[EMAIL]", text))\n# [EMAIL], [EMAIL], [EMAIL]', 're.findall(r"\\d", "abc123") # [\'1\', \'2\', \'3\']\nre.findall(r"\\d+", "abc123 def456") # [\'123\', \'456\']\nre.findall(r"[aeiou]", "regex lesson") # [\'e\', \'e\', \'e\', \'o\']\nre.findall(r"[^aeiou\\s]", "regex") # [\'r\', \'g\', \'x\'] (the consonants)', 'text = "<b>bold</b> and <i>italic</i>"\n\nre.findall(r"<.*>", text) # [\'<b>bold</b> and <i>italic</i>\'] (greedy)\nre.findall(r"<.*?>", text) # [\'<b>\', \'</b>\', \'<i>\', \'</i>\'] (lazy)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
^ and $ by default match the start/end of the whole string. With re.MULTILINE, they match the start/end of each line:
log = "ERROR: x\nINFO: y\nERROR: z" re.findall(r"^ERROR", log) # ['ERROR'] (only the ERROR on line 1) re.findall(r"^ERROR", log, re.MULTILINE) # ['ERROR', 'ERROR']
setup added so this can run · re-runs 5 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) re = _AutoMock('re') # Earlier blocks on this page that this one builds on, re-run quietly first. try: import re except Exception: pass _pm_earlier = [ 'import re\n\ntext = "Order 4271 shipped on 2026-03-14"\n\nm = re.search(r"\\d+", text) # find the first run of digits\nprint(m) # <re.Match object; span=(6, 10), match=\'4271\'>\nprint(m.group()) # \'4271\'\nprint(m.span()) # (6, 10) (the start and end index)', 'import re\n\ntext = "alice@example.com, bob@example.com, carol@other.org"\n\n# 1. search: the first match anywhere, or None\nm = re.search(r"\\w+@\\w+\\.\\w+", text)\nprint(m.group()) # alice@example.com\n\n# 2. match: a match only at the START of the string\nprint(re.match(r"bob@", text)) # None (match only checks at index 0, and text starts with "alice@")\nprint(re.match(r"alice", text)) # Match object\n\n# 3. findall: every match, as a list of strings\nprint(re.findall(r"\\w+@\\w+\\.\\w+", text))\n# [\'alice@example.com\', \'bob@example.com\', \'carol@other.org\']\n\n# 4. finditer: every match, as Match objects (best when you need positions)\nfor m in re.finditer(r"\\w+@\\w+\\.\\w+", text):\n print(m.group(), "at", m.span())\n\n# 5. sub: find and replace\nprint(re.sub(r"\\w+@\\w+\\.\\w+", "[EMAIL]", text))\n# [EMAIL], [EMAIL], [EMAIL]', 're.findall(r"\\d", "abc123") # [\'1\', \'2\', \'3\']\nre.findall(r"\\d+", "abc123 def456") # [\'123\', \'456\']\nre.findall(r"[aeiou]", "regex lesson") # [\'e\', \'e\', \'e\', \'o\']\nre.findall(r"[^aeiou\\s]", "regex") # [\'r\', \'g\', \'x\'] (the consonants)', 'text = "<b>bold</b> and <i>italic</i>"\n\nre.findall(r"<.*>", text) # [\'<b>bold</b> and <i>italic</i>\'] (greedy)\nre.findall(r"<.*?>", text) # [\'<b>\', \'</b>\', \'<i>\', \'</i>\'] (lazy)', 'text = "cat caterpillar scattered cats"\n\nre.findall(r"cat", text) # [\'cat\', \'cat\', \'cat\', \'cat\'] (too many)\nre.findall(r"\\bcat\\b", text) # [\'cat\'] (only the whole word)\nre.findall(r"\\bcat", text) # [\'cat\', \'cat\', \'cat\'] (cat at the start of a word)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
6. Groups: Capturing Pieces
Parentheses (...) create a capture group: a part of the match that you can get by its number.
m = re.search(r"(\d{4})-(\d{2})-(\d{2})", "Released 2026-03-14, today.") print(m.group(0)) # 2026-03-14 (the full match) print(m.group(1)) # 2026 (the first group) print(m.group(2)) # 03 (the second group) print(m.group(3)) # 14 (the third group) print(m.groups()) # ('2026', '03', '14')
setup added so this can run · re-runs 6 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) re = _AutoMock('re') # Earlier blocks on this page that this one builds on, re-run quietly first. try: import re except Exception: pass _pm_earlier = [ 'import re\n\ntext = "Order 4271 shipped on 2026-03-14"\n\nm = re.search(r"\\d+", text) # find the first run of digits\nprint(m) # <re.Match object; span=(6, 10), match=\'4271\'>\nprint(m.group()) # \'4271\'\nprint(m.span()) # (6, 10) (the start and end index)', 'import re\n\ntext = "alice@example.com, bob@example.com, carol@other.org"\n\n# 1. search: the first match anywhere, or None\nm = re.search(r"\\w+@\\w+\\.\\w+", text)\nprint(m.group()) # alice@example.com\n\n# 2. match: a match only at the START of the string\nprint(re.match(r"bob@", text)) # None (match only checks at index 0, and text starts with "alice@")\nprint(re.match(r"alice", text)) # Match object\n\n# 3. findall: every match, as a list of strings\nprint(re.findall(r"\\w+@\\w+\\.\\w+", text))\n# [\'alice@example.com\', \'bob@example.com\', \'carol@other.org\']\n\n# 4. finditer: every match, as Match objects (best when you need positions)\nfor m in re.finditer(r"\\w+@\\w+\\.\\w+", text):\n print(m.group(), "at", m.span())\n\n# 5. sub: find and replace\nprint(re.sub(r"\\w+@\\w+\\.\\w+", "[EMAIL]", text))\n# [EMAIL], [EMAIL], [EMAIL]', 're.findall(r"\\d", "abc123") # [\'1\', \'2\', \'3\']\nre.findall(r"\\d+", "abc123 def456") # [\'123\', \'456\']\nre.findall(r"[aeiou]", "regex lesson") # [\'e\', \'e\', \'e\', \'o\']\nre.findall(r"[^aeiou\\s]", "regex") # [\'r\', \'g\', \'x\'] (the consonants)', 'text = "<b>bold</b> and <i>italic</i>"\n\nre.findall(r"<.*>", text) # [\'<b>bold</b> and <i>italic</i>\'] (greedy)\nre.findall(r"<.*?>", text) # [\'<b>\', \'</b>\', \'<i>\', \'</i>\'] (lazy)', 'text = "cat caterpillar scattered cats"\n\nre.findall(r"cat", text) # [\'cat\', \'cat\', \'cat\', \'cat\'] (too many)\nre.findall(r"\\bcat\\b", text) # [\'cat\'] (only the whole word)\nre.findall(r"\\bcat", text) # [\'cat\', \'cat\', \'cat\'] (cat at the start of a word)', 'log = "ERROR: x\\nINFO: y\\nERROR: z"\n\nre.findall(r"^ERROR", log) # [\'ERROR\'] (only the ERROR on line 1)\nre.findall(r"^ERROR", log, re.MULTILINE) # [\'ERROR\', \'ERROR\']', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
Non-capturing groups (?:...) group without capturing. They are useful when you need a group for alternation (|) or a quantifier, but don't want the result in .groups():
re.findall(r"(?:Mr|Mrs|Ms)\. (\w+)", "Mr. Stark, Ms. Potts") # ['Stark', 'Potts'] (only the name is captured, not the title)
setup added so this can run · re-runs 7 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) re = _AutoMock('re') # Earlier blocks on this page that this one builds on, re-run quietly first. try: import re except Exception: pass _pm_earlier = [ 'import re\n\ntext = "Order 4271 shipped on 2026-03-14"\n\nm = re.search(r"\\d+", text) # find the first run of digits\nprint(m) # <re.Match object; span=(6, 10), match=\'4271\'>\nprint(m.group()) # \'4271\'\nprint(m.span()) # (6, 10) (the start and end index)', 'import re\n\ntext = "alice@example.com, bob@example.com, carol@other.org"\n\n# 1. search: the first match anywhere, or None\nm = re.search(r"\\w+@\\w+\\.\\w+", text)\nprint(m.group()) # alice@example.com\n\n# 2. match: a match only at the START of the string\nprint(re.match(r"bob@", text)) # None (match only checks at index 0, and text starts with "alice@")\nprint(re.match(r"alice", text)) # Match object\n\n# 3. findall: every match, as a list of strings\nprint(re.findall(r"\\w+@\\w+\\.\\w+", text))\n# [\'alice@example.com\', \'bob@example.com\', \'carol@other.org\']\n\n# 4. finditer: every match, as Match objects (best when you need positions)\nfor m in re.finditer(r"\\w+@\\w+\\.\\w+", text):\n print(m.group(), "at", m.span())\n\n# 5. sub: find and replace\nprint(re.sub(r"\\w+@\\w+\\.\\w+", "[EMAIL]", text))\n# [EMAIL], [EMAIL], [EMAIL]', 're.findall(r"\\d", "abc123") # [\'1\', \'2\', \'3\']\nre.findall(r"\\d+", "abc123 def456") # [\'123\', \'456\']\nre.findall(r"[aeiou]", "regex lesson") # [\'e\', \'e\', \'e\', \'o\']\nre.findall(r"[^aeiou\\s]", "regex") # [\'r\', \'g\', \'x\'] (the consonants)', 'text = "<b>bold</b> and <i>italic</i>"\n\nre.findall(r"<.*>", text) # [\'<b>bold</b> and <i>italic</i>\'] (greedy)\nre.findall(r"<.*?>", text) # [\'<b>\', \'</b>\', \'<i>\', \'</i>\'] (lazy)', 'text = "cat caterpillar scattered cats"\n\nre.findall(r"cat", text) # [\'cat\', \'cat\', \'cat\', \'cat\'] (too many)\nre.findall(r"\\bcat\\b", text) # [\'cat\'] (only the whole word)\nre.findall(r"\\bcat", text) # [\'cat\', \'cat\', \'cat\'] (cat at the start of a word)', 'log = "ERROR: x\\nINFO: y\\nERROR: z"\n\nre.findall(r"^ERROR", log) # [\'ERROR\'] (only the ERROR on line 1)\nre.findall(r"^ERROR", log, re.MULTILINE) # [\'ERROR\', \'ERROR\']', 'm = re.search(r"(\\d{4})-(\\d{2})-(\\d{2})", "Released 2026-03-14, today.")\nprint(m.group(0)) # 2026-03-14 (the full match)\nprint(m.group(1)) # 2026 (the first group)\nprint(m.group(2)) # 03 (the second group)\nprint(m.group(3)) # 14 (the third group)\nprint(m.groups()) # (\'2026\', \'03\', \'14\')', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
Named groups (?P<name>...) are easier to read. You access them by name instead of by number, so adding a group doesn't break your code:
m = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})", "2026-03-14") print(m.group("year")) # 2026 print(m.groupdict()) # {'year': '2026', 'month': '03', 'day': '14'}
setup added so this can run · re-runs 8 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) re = _AutoMock('re') # Earlier blocks on this page that this one builds on, re-run quietly first. try: import re except Exception: pass _pm_earlier = [ 'import re\n\ntext = "Order 4271 shipped on 2026-03-14"\n\nm = re.search(r"\\d+", text) # find the first run of digits\nprint(m) # <re.Match object; span=(6, 10), match=\'4271\'>\nprint(m.group()) # \'4271\'\nprint(m.span()) # (6, 10) (the start and end index)', 'import re\n\ntext = "alice@example.com, bob@example.com, carol@other.org"\n\n# 1. search: the first match anywhere, or None\nm = re.search(r"\\w+@\\w+\\.\\w+", text)\nprint(m.group()) # alice@example.com\n\n# 2. match: a match only at the START of the string\nprint(re.match(r"bob@", text)) # None (match only checks at index 0, and text starts with "alice@")\nprint(re.match(r"alice", text)) # Match object\n\n# 3. findall: every match, as a list of strings\nprint(re.findall(r"\\w+@\\w+\\.\\w+", text))\n# [\'alice@example.com\', \'bob@example.com\', \'carol@other.org\']\n\n# 4. finditer: every match, as Match objects (best when you need positions)\nfor m in re.finditer(r"\\w+@\\w+\\.\\w+", text):\n print(m.group(), "at", m.span())\n\n# 5. sub: find and replace\nprint(re.sub(r"\\w+@\\w+\\.\\w+", "[EMAIL]", text))\n# [EMAIL], [EMAIL], [EMAIL]', 're.findall(r"\\d", "abc123") # [\'1\', \'2\', \'3\']\nre.findall(r"\\d+", "abc123 def456") # [\'123\', \'456\']\nre.findall(r"[aeiou]", "regex lesson") # [\'e\', \'e\', \'e\', \'o\']\nre.findall(r"[^aeiou\\s]", "regex") # [\'r\', \'g\', \'x\'] (the consonants)', 'text = "<b>bold</b> and <i>italic</i>"\n\nre.findall(r"<.*>", text) # [\'<b>bold</b> and <i>italic</i>\'] (greedy)\nre.findall(r"<.*?>", text) # [\'<b>\', \'</b>\', \'<i>\', \'</i>\'] (lazy)', 'text = "cat caterpillar scattered cats"\n\nre.findall(r"cat", text) # [\'cat\', \'cat\', \'cat\', \'cat\'] (too many)\nre.findall(r"\\bcat\\b", text) # [\'cat\'] (only the whole word)\nre.findall(r"\\bcat", text) # [\'cat\', \'cat\', \'cat\'] (cat at the start of a word)', 'log = "ERROR: x\\nINFO: y\\nERROR: z"\n\nre.findall(r"^ERROR", log) # [\'ERROR\'] (only the ERROR on line 1)\nre.findall(r"^ERROR", log, re.MULTILINE) # [\'ERROR\', \'ERROR\']', 'm = re.search(r"(\\d{4})-(\\d{2})-(\\d{2})", "Released 2026-03-14, today.")\nprint(m.group(0)) # 2026-03-14 (the full match)\nprint(m.group(1)) # 2026 (the first group)\nprint(m.group(2)) # 03 (the second group)\nprint(m.group(3)) # 14 (the third group)\nprint(m.groups()) # (\'2026\', \'03\', \'14\')', 're.findall(r"(?:Mr|Mrs|Ms)\\. (\\w+)", "Mr. Stark, Ms. Potts")\n# [\'Stark\', \'Potts\'] (only the name is captured, not the title)', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
If you have more than two groups, name them. Mixing up group 1 and group 2 is a very common bug.
7. Backreferences
Once you've captured a group, you can refer to it later: inside the pattern (to require the same text again) and in a replacement string (to reuse the captured text).
# In the pattern: find words that are immediately repeated re.findall(r"\b(\w+) \1\b", "the the cat sat on on the mat") # ['the', 'on'] # In a replacement: swap first and last name re.sub(r"(\w+) (\w+)", r"\2 \1", "Linus Teja") # 'Teja Linus' # Named variant re.sub(r"(?P<first>\w+) (?P<last>\w+)", r"\g<last> \g<first>", "Linus Teja") # 'Teja Linus'
setup added so this can run · re-runs 9 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) re = _AutoMock('re') # Earlier blocks on this page that this one builds on, re-run quietly first. try: import re except Exception: pass _pm_earlier = [ 'import re\n\ntext = "Order 4271 shipped on 2026-03-14"\n\nm = re.search(r"\\d+", text) # find the first run of digits\nprint(m) # <re.Match object; span=(6, 10), match=\'4271\'>\nprint(m.group()) # \'4271\'\nprint(m.span()) # (6, 10) (the start and end index)', 'import re\n\ntext = "alice@example.com, bob@example.com, carol@other.org"\n\n# 1. search: the first match anywhere, or None\nm = re.search(r"\\w+@\\w+\\.\\w+", text)\nprint(m.group()) # alice@example.com\n\n# 2. match: a match only at the START of the string\nprint(re.match(r"bob@", text)) # None (match only checks at index 0, and text starts with "alice@")\nprint(re.match(r"alice", text)) # Match object\n\n# 3. findall: every match, as a list of strings\nprint(re.findall(r"\\w+@\\w+\\.\\w+", text))\n# [\'alice@example.com\', \'bob@example.com\', \'carol@other.org\']\n\n# 4. finditer: every match, as Match objects (best when you need positions)\nfor m in re.finditer(r"\\w+@\\w+\\.\\w+", text):\n print(m.group(), "at", m.span())\n\n# 5. sub: find and replace\nprint(re.sub(r"\\w+@\\w+\\.\\w+", "[EMAIL]", text))\n# [EMAIL], [EMAIL], [EMAIL]', 're.findall(r"\\d", "abc123") # [\'1\', \'2\', \'3\']\nre.findall(r"\\d+", "abc123 def456") # [\'123\', \'456\']\nre.findall(r"[aeiou]", "regex lesson") # [\'e\', \'e\', \'e\', \'o\']\nre.findall(r"[^aeiou\\s]", "regex") # [\'r\', \'g\', \'x\'] (the consonants)', 'text = "<b>bold</b> and <i>italic</i>"\n\nre.findall(r"<.*>", text) # [\'<b>bold</b> and <i>italic</i>\'] (greedy)\nre.findall(r"<.*?>", text) # [\'<b>\', \'</b>\', \'<i>\', \'</i>\'] (lazy)', 'text = "cat caterpillar scattered cats"\n\nre.findall(r"cat", text) # [\'cat\', \'cat\', \'cat\', \'cat\'] (too many)\nre.findall(r"\\bcat\\b", text) # [\'cat\'] (only the whole word)\nre.findall(r"\\bcat", text) # [\'cat\', \'cat\', \'cat\'] (cat at the start of a word)', 'log = "ERROR: x\\nINFO: y\\nERROR: z"\n\nre.findall(r"^ERROR", log) # [\'ERROR\'] (only the ERROR on line 1)\nre.findall(r"^ERROR", log, re.MULTILINE) # [\'ERROR\', \'ERROR\']', 'm = re.search(r"(\\d{4})-(\\d{2})-(\\d{2})", "Released 2026-03-14, today.")\nprint(m.group(0)) # 2026-03-14 (the full match)\nprint(m.group(1)) # 2026 (the first group)\nprint(m.group(2)) # 03 (the second group)\nprint(m.group(3)) # 14 (the third group)\nprint(m.groups()) # (\'2026\', \'03\', \'14\')', 're.findall(r"(?:Mr|Mrs|Ms)\\. (\\w+)", "Mr. Stark, Ms. Potts")\n# [\'Stark\', \'Potts\'] (only the name is captured, not the title)', 'm = re.search(r"(?P<year>\\d{4})-(?P<month>\\d{2})-(?P<day>\\d{2})", "2026-03-14")\nprint(m.group("year")) # 2026\nprint(m.groupdict()) # {\'year\': \'2026\', \'month\': \'03\', \'day\': \'14\'}', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
\1 is the first captured group, and \g<name> is the named-group form in replacement strings. With backreferences, a regex can describe not only the shape of the text but also relationships between parts of it (for example, "the same word twice").
8. Flags
Flags change how the engine reads the pattern. Pass them as the third argument. To use several flags, combine them with |.
| Flag | Effect |
|---|---|
re.IGNORECASE (or re.I) | Case-insensitive matching. |
re.MULTILINE (or re.M) | ^ and $ match line boundaries, not just string. |
re.DOTALL (or re.S) | . matches newlines too. |
re.VERBOSE (or re.X) | Allow whitespace + # comments inside the pattern. |
re.findall(r"hello", "Hello world", re.IGNORECASE) # ['Hello'] re.findall(r".+", "line1\nline2", re.DOTALL) # ['line1\nline2']
setup added so this can run · re-runs 10 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) re = _AutoMock('re') # Earlier blocks on this page that this one builds on, re-run quietly first. try: import re except Exception: pass _pm_earlier = [ 'import re\n\ntext = "Order 4271 shipped on 2026-03-14"\n\nm = re.search(r"\\d+", text) # find the first run of digits\nprint(m) # <re.Match object; span=(6, 10), match=\'4271\'>\nprint(m.group()) # \'4271\'\nprint(m.span()) # (6, 10) (the start and end index)', 'import re\n\ntext = "alice@example.com, bob@example.com, carol@other.org"\n\n# 1. search: the first match anywhere, or None\nm = re.search(r"\\w+@\\w+\\.\\w+", text)\nprint(m.group()) # alice@example.com\n\n# 2. match: a match only at the START of the string\nprint(re.match(r"bob@", text)) # None (match only checks at index 0, and text starts with "alice@")\nprint(re.match(r"alice", text)) # Match object\n\n# 3. findall: every match, as a list of strings\nprint(re.findall(r"\\w+@\\w+\\.\\w+", text))\n# [\'alice@example.com\', \'bob@example.com\', \'carol@other.org\']\n\n# 4. finditer: every match, as Match objects (best when you need positions)\nfor m in re.finditer(r"\\w+@\\w+\\.\\w+", text):\n print(m.group(), "at", m.span())\n\n# 5. sub: find and replace\nprint(re.sub(r"\\w+@\\w+\\.\\w+", "[EMAIL]", text))\n# [EMAIL], [EMAIL], [EMAIL]', 're.findall(r"\\d", "abc123") # [\'1\', \'2\', \'3\']\nre.findall(r"\\d+", "abc123 def456") # [\'123\', \'456\']\nre.findall(r"[aeiou]", "regex lesson") # [\'e\', \'e\', \'e\', \'o\']\nre.findall(r"[^aeiou\\s]", "regex") # [\'r\', \'g\', \'x\'] (the consonants)', 'text = "<b>bold</b> and <i>italic</i>"\n\nre.findall(r"<.*>", text) # [\'<b>bold</b> and <i>italic</i>\'] (greedy)\nre.findall(r"<.*?>", text) # [\'<b>\', \'</b>\', \'<i>\', \'</i>\'] (lazy)', 'text = "cat caterpillar scattered cats"\n\nre.findall(r"cat", text) # [\'cat\', \'cat\', \'cat\', \'cat\'] (too many)\nre.findall(r"\\bcat\\b", text) # [\'cat\'] (only the whole word)\nre.findall(r"\\bcat", text) # [\'cat\', \'cat\', \'cat\'] (cat at the start of a word)', 'log = "ERROR: x\\nINFO: y\\nERROR: z"\n\nre.findall(r"^ERROR", log) # [\'ERROR\'] (only the ERROR on line 1)\nre.findall(r"^ERROR", log, re.MULTILINE) # [\'ERROR\', \'ERROR\']', 'm = re.search(r"(\\d{4})-(\\d{2})-(\\d{2})", "Released 2026-03-14, today.")\nprint(m.group(0)) # 2026-03-14 (the full match)\nprint(m.group(1)) # 2026 (the first group)\nprint(m.group(2)) # 03 (the second group)\nprint(m.group(3)) # 14 (the third group)\nprint(m.groups()) # (\'2026\', \'03\', \'14\')', 're.findall(r"(?:Mr|Mrs|Ms)\\. (\\w+)", "Mr. Stark, Ms. Potts")\n# [\'Stark\', \'Potts\'] (only the name is captured, not the title)', 'm = re.search(r"(?P<year>\\d{4})-(?P<month>\\d{2})-(?P<day>\\d{2})", "2026-03-14")\nprint(m.group("year")) # 2026\nprint(m.groupdict()) # {\'year\': \'2026\', \'month\': \'03\', \'day\': \'14\'}', '# In the pattern: find words that are immediately repeated\nre.findall(r"\\b(\\w+) \\1\\b", "the the cat sat on on the mat")\n# [\'the\', \'on\']\n\n# In a replacement: swap first and last name\nre.sub(r"(\\w+) (\\w+)", r"\\2 \\1", "Linus Teja")\n# \'Teja Linus\'\n\n# Named variant\nre.sub(r"(?P<first>\\w+) (?P<last>\\w+)",\n r"\\g<last> \\g<first>",\n "Linus Teja")\n# \'Teja Linus\'', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
Many people don't know about re.VERBOSE. It makes long, complex patterns readable:
phone_pattern = re.compile(r""" \+? # optional leading + (?P<country>\d{1,3}) # 1-3 digit country code [\s\-]? # optional separator (?P<area>\d{3}) # area code [\s\-]? # optional separator (?P<line>\d{4}) # line number """, re.VERBOSE) m = phone_pattern.search("Call me at +1 555-7890 maybe") print(m.groupdict()) # {'country': '1', 'area': '555', 'line': '7890'}
setup added so this can run · re-runs 11 earlier blocks on this page first
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) re = _AutoMock('re') # Earlier blocks on this page that this one builds on, re-run quietly first. try: import re except Exception: pass _pm_earlier = [ 'import re\n\ntext = "Order 4271 shipped on 2026-03-14"\n\nm = re.search(r"\\d+", text) # find the first run of digits\nprint(m) # <re.Match object; span=(6, 10), match=\'4271\'>\nprint(m.group()) # \'4271\'\nprint(m.span()) # (6, 10) (the start and end index)', 'import re\n\ntext = "alice@example.com, bob@example.com, carol@other.org"\n\n# 1. search: the first match anywhere, or None\nm = re.search(r"\\w+@\\w+\\.\\w+", text)\nprint(m.group()) # alice@example.com\n\n# 2. match: a match only at the START of the string\nprint(re.match(r"bob@", text)) # None (match only checks at index 0, and text starts with "alice@")\nprint(re.match(r"alice", text)) # Match object\n\n# 3. findall: every match, as a list of strings\nprint(re.findall(r"\\w+@\\w+\\.\\w+", text))\n# [\'alice@example.com\', \'bob@example.com\', \'carol@other.org\']\n\n# 4. finditer: every match, as Match objects (best when you need positions)\nfor m in re.finditer(r"\\w+@\\w+\\.\\w+", text):\n print(m.group(), "at", m.span())\n\n# 5. sub: find and replace\nprint(re.sub(r"\\w+@\\w+\\.\\w+", "[EMAIL]", text))\n# [EMAIL], [EMAIL], [EMAIL]', 're.findall(r"\\d", "abc123") # [\'1\', \'2\', \'3\']\nre.findall(r"\\d+", "abc123 def456") # [\'123\', \'456\']\nre.findall(r"[aeiou]", "regex lesson") # [\'e\', \'e\', \'e\', \'o\']\nre.findall(r"[^aeiou\\s]", "regex") # [\'r\', \'g\', \'x\'] (the consonants)', 'text = "<b>bold</b> and <i>italic</i>"\n\nre.findall(r"<.*>", text) # [\'<b>bold</b> and <i>italic</i>\'] (greedy)\nre.findall(r"<.*?>", text) # [\'<b>\', \'</b>\', \'<i>\', \'</i>\'] (lazy)', 'text = "cat caterpillar scattered cats"\n\nre.findall(r"cat", text) # [\'cat\', \'cat\', \'cat\', \'cat\'] (too many)\nre.findall(r"\\bcat\\b", text) # [\'cat\'] (only the whole word)\nre.findall(r"\\bcat", text) # [\'cat\', \'cat\', \'cat\'] (cat at the start of a word)', 'log = "ERROR: x\\nINFO: y\\nERROR: z"\n\nre.findall(r"^ERROR", log) # [\'ERROR\'] (only the ERROR on line 1)\nre.findall(r"^ERROR", log, re.MULTILINE) # [\'ERROR\', \'ERROR\']', 'm = re.search(r"(\\d{4})-(\\d{2})-(\\d{2})", "Released 2026-03-14, today.")\nprint(m.group(0)) # 2026-03-14 (the full match)\nprint(m.group(1)) # 2026 (the first group)\nprint(m.group(2)) # 03 (the second group)\nprint(m.group(3)) # 14 (the third group)\nprint(m.groups()) # (\'2026\', \'03\', \'14\')', 're.findall(r"(?:Mr|Mrs|Ms)\\. (\\w+)", "Mr. Stark, Ms. Potts")\n# [\'Stark\', \'Potts\'] (only the name is captured, not the title)', 'm = re.search(r"(?P<year>\\d{4})-(?P<month>\\d{2})-(?P<day>\\d{2})", "2026-03-14")\nprint(m.group("year")) # 2026\nprint(m.groupdict()) # {\'year\': \'2026\', \'month\': \'03\', \'day\': \'14\'}', '# In the pattern: find words that are immediately repeated\nre.findall(r"\\b(\\w+) \\1\\b", "the the cat sat on on the mat")\n# [\'the\', \'on\']\n\n# In a replacement: swap first and last name\nre.sub(r"(\\w+) (\\w+)", r"\\2 \\1", "Linus Teja")\n# \'Teja Linus\'\n\n# Named variant\nre.sub(r"(?P<first>\\w+) (?P<last>\\w+)",\n r"\\g<last> \\g<first>",\n "Linus Teja")\n# \'Teja Linus\'', 're.findall(r"hello", "Hello world", re.IGNORECASE) # [\'Hello\']\nre.findall(r".+", "line1\\nline2", re.DOTALL) # [\'line1\\nline2\']', ] def _pm_run_earlier(_srcs, _g): import sys as _s, io as _io, warnings as _w for _src in _srcs: _o, _e = _s.stdout, _s.stderr _s.stdout = _s.stderr = _io.StringIO() try: with _w.catch_warnings(): _w.simplefilter('ignore') exec(compile(_src, '<earlier block>', 'exec'), _g) except (Exception, SystemExit): pass finally: _s.stdout, _s.stderr = _o, _e if 'matplotlib.pyplot' in _s.modules: _s.modules['matplotlib.pyplot'].close('all') # their plots belong to their own blocks _pm_run_earlier(_pm_earlier, globals()) del _pm_earlier, _pm_run_earlier
Whitespace and #-comments inside the pattern are ignored. To match a literal space, write \ or [ ]. Any complex pattern that stays in your code for more than a week should be in re.VERBOSE form, so that you can still read it later.
9. re.compile: Compile Once for Busy Loops
re.search, re.findall, etc. keep a cache of compiled patterns, so calling them in a loop is fine, up to a point. For loops that run a very large number of times, or to give a pattern a meaningful name, compile it once and reuse it:
EMAIL_RE = re.compile(r"\b[\w.+-]+@[\w-]+\.[\w.-]+\b") for line in big_log_file: for match in EMAIL_RE.finditer(line): process(match.group())
setup added so this can run · defines big_log_file, re, process
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) big_log_file = ["alpha", "beta", "gamma"] re = _AutoMock('re') def process(*_a, **_kw): print('-> process() called') return _AutoMock('process()')
Compiled patterns have the same operations as the module-level functions: EMAIL_RE.search, .findall, .finditer, .sub. They are also normal objects: you can pass them around, store them in dicts, and, most importantly, name them, so the reader of your code knows what you're matching, not just how.
10. Always Use Raw Strings: r"..."
Without r, Python's string literal parser interprets backslashes first:
# Wrong: Python reads "\d" inside a normal string. "\d" is not a known escape, # so it stays as backslash-d, BUT this raises SyntaxWarning (since 3.12) and will # eventually be a SyntaxError. And "\b" IS a known escape (backspace, \x08), # so Python turns it into a backspace before the regex ever sees it. pattern = "\bword\b" # backspace, "word", backspace (no \b word boundaries) # Right: a raw string keeps the backslashes as they are pattern = r"\bword\b"
The fix is one character. Add r in front of every regex pattern, every time, until you do it without thinking.
The same applies to replacement strings in re.sub if they contain backslashes:
re.sub(r"(\d+)", r"<\1>", "got 42 items") # 'got <42> items'
setup added so this can run · defines re
# Lightweight mock for objects whose attributes/methods aren't critical class _AutoMock: def __init__(self, name='mock'): self._name = name def __getattr__(self, k): return _AutoMock(self._name + '.' + k) def __call__(self, *a, **kw): print('-> ' + self._name + '() called') return _AutoMock(self._name + '()') def __repr__(self): return '<mock ' + self._name + '>' def __str__(self): return '<mock ' + self._name + '>' def __bool__(self): return True def __iter__(self): return iter([]) def __len__(self): return 0 def __getitem__(self, k): return _AutoMock(self._name + '[...]') def __setitem__(self, k, v): pass def __enter__(self): return self def __exit__(self, *a): return False async def __aenter__(self): return self async def __aexit__(self, *a): return False def __add__(self, o): return self def __radd__(self, o): return self def __sub__(self, o): return self def __mul__(self, o): return self def __rmul__(self, o): return self def __truediv__(self, o): return self def __eq__(self, o): return isinstance(o, _AutoMock) def __hash__(self): return hash(self._name) def __lt__(self, o): return True def __le__(self, o): return True def __gt__(self, o): return False def __ge__(self, o): return False def __mro_entries__(self, bases): return (object,) re = _AutoMock('re')
11. When NOT to Use Regex
This section may be the most useful one in the lesson. Regex matches patterns. It does not parse structure.
| You want to… | Use… | Not regex because… |
|---|---|---|
| Parse HTML / XML | BeautifulSoup, lxml (see scraping) | HTML is nested, regex isn't. Your regex will handle the simple cases and fail on the hard ones. |
| Parse JSON | json module | JSON is structured, and json.loads validates and decodes in one call. |
| Parse CSV | csv module | Embedded commas, quoted fields, multi-line cells: csv handles them, and a regex will miss some of them. |
| Validate emails "properly" | Send a confirmation email | The RFC 5322 grammar is long and complex. Most email regexes reject some valid addresses or accept some invalid ones. The only real test is "does the email arrive?" |
| Parse Python / SQL / any code | An actual parser (ast, sqlglot) | Code is hierarchical, but regex is flat. |
| Extract a number from a sentence | Regex ✓ | This is a simple, flat pattern, so regex is a good choice. |
The rule of thumb: if the input has nested or hierarchical structure, regex is the wrong tool. If it has flat patterns that you can recognise without context, regex is excellent.
12. Real-World Patterns You Will Reuse
import re # Split on any run of whitespace (handles tabs, multiple spaces, newlines) re.split(r"\s+", "hello world\tfoo\nbar") # ['hello', 'world', 'foo', 'bar'] # Strip multiple spaces down to one re.sub(r"\s+", " ", "hello world ").strip() # 'hello world' # Find URLs (a simple pattern for common cases, not every valid URL) URL_RE = re.compile(r"https?://[^\s<>\"']+") URL_RE.findall("see https://example.com/x and http://foo.bar/baz here") # ['https://example.com/x', 'http://foo.bar/baz'] # Extract YYYY-MM-DD dates re.findall(r"\b(\d{4})-(\d{2})-(\d{2})\b", "log 2026-03-14 and 2025-12-01") # [('2026', '03', '14'), ('2025', '12', '01')] # Escape user input for safe inclusion in a pattern user_query = "C++ 3.0 (final)" safe = re.escape(user_query) # 'C\\+\\+\\ 3\\.0\\ \\(final\\)' # Now it is safe to put in a pattern: re.search(rf"^{safe}$", line)
Always use re.escape() when you insert text from a user into a pattern. Without it, a user typing . or ( would have those treated as regex metacharacters, and a user typing .* could match anything.
13. Common Mistakes
1. Forgetting the r prefix"\d" is a deprecated escape in regular strings, but r"\d" is correct. The fix is one character, and the bug it prevents can be hard to see ("\b" becomes the ASCII backspace character).
2. Greedy quantifiers matching too much<.*> over <b>bold</b> matches the whole thing, not just <b>. Use <.*?> for lazy, or be explicit: <[^>]*>.
3. Anchoring without re.MULTILINE^ERROR on a multi-line log only matches the very first line. Pass re.MULTILINE when you mean per-line.
4. Unescaped special characters
A literal dot, plus, parenthesis, brace, bracket or pipe needs a \ in front of it. Or use [.], [+], etc. Or re.escape(s) for runtime strings.
5. Treating regex as a parser
HTML, JSON, CSV, source code, math expressions and balanced parentheses are not jobs for regex. Use a parser made for the format.
6. re.match vs re.search confusionmatch is anchored at the start of the string. If your pattern doesn't start with ^, match still requires the match to begin at index 0. Use search when you mean "find anywhere".
🎯 Your Turn: Find All Emails
Write find_emails(text) that returns a list of deduplicated, lowercased email addresses found in text. The regex should handle:
- Standard local parts:
alice@example.com - Plus-tags:
alice+filter@example.com - Dots in the local part:
first.last@example.com - Subdomains:
alice@mail.eng.example.com - Mixed case (should be returned lowercase)
Provide the regex inline with comments using re.VERBOSE.
text = """ Contact alice@example.com or ALICE@EXAMPLE.COM (same person). CC bob+team@mail.example.co.uk and first.last@sub.eng.example.com. Not an email: alice@, @example.com, just-a-word. """ # find_emails(text) returns ['alice@example.com', # 'bob+team@mail.example.co.uk', # 'first.last@sub.eng.example.com']
Skeleton:
import re EMAIL_RE = re.compile(r""" # TODO 1: match the local part (letters, digits, dots, pluses, hyphens, underscores) # TODO 2: an @ sign # TODO 3: a domain, made of one or more labels separated by dots # TODO 4: ensure a TLD of at least 2 letters at the end """, re.VERBOSE) def find_emails(text): # TODO 5: findall, make lowercase, remove duplicates (keep the order) ...
Hint 1: Local part character class
Allowed characters in the local part (for our purposes):[\w.+\-]+. \w covers letters, digits, and underscore. We add ., +, and - explicitly. Note: - inside a character class is fine at the end or escaped.
Hint 2: Domain with multiple labels
A domain likemail.eng.example.com is a series of label-then-dot, ending in a final TLD label. One way: (?:[\w-]+\.)+[A-Za-z]{2,} means one or more "label." groups followed by a final TLD of 2 or more letters.
Hint 3: Remove duplicates but keep the order
Aset removes duplicates but loses the order. To remove duplicates and keep the first-seen order: build a dict.fromkeys(items) and take its keys. Dicts in Python 3.7+ preserve insertion order.
Show full solution
import re EMAIL_RE = re.compile(r""" \b # word boundary so we don't match mid-word ( [\w.+\-]+ # local part: letters, digits, _ . + - @ (?:[\w\-]+\.)+ # one or more "subdomain." labels [A-Za-z]{2,} # top-level domain (TLD): 2 or more letters ) \b """, re.VERBOSE) def find_emails(text): """Return deduplicated, lowercase emails in first-seen order.""" matches = (m.group(1).lower() for m in EMAIL_RE.finditer(text)) return list(dict.fromkeys(matches)) # Demo text = """ Contact alice@example.com or ALICE@EXAMPLE.COM (same person). CC bob+team@mail.example.co.uk and first.last@sub.eng.example.com. Not an email: alice@, @example.com, just-a-word. """ for e in find_emails(text): print(e) # alice@example.com # bob+team@mail.example.co.uk # first.last@sub.eng.example.com
What's going on:
re.VERBOSElets us comment each piece of the pattern. Six months from now, you can still read and change this regex.\bword boundaries stop the match from starting or ending in the middle of a word, for example in the trailing punctuation in(alice@example.com).[\w.+\-]+is the local part. The\-is escaped (it could also go last:[\w.+-]+works too). The other characters are literal inside the class.(?:[\w\-]+\.)+means "one or more labels, each followed by a dot": a non-capturing group, repeated. It handles any number of subdomains.[A-Za-z]{2,}requires the TLD to be at least two letters. This rejects invalid input likealice@example.1.dict.fromkeys(...)is the standard way to remove duplicates and keep the order in modern Python. We could have used a loop with aseenset, but this is one line.
Note: this regex is good for common cases, but it does not follow the full RFC. RFC 5322 allows quoted local parts ("weird name"@example.com), IP literals (bob@[192.168.1.1]), and other unusual forms you'll almost never see in practice. If you really need to check that an email address works, send a confirmation email. That is the only check that is always correct.
What You Learned
remodule with five functions:search,match,findall,finditer,sub.- Character classes (
\d \w \s .,[abc],[^abc],[a-z]) describe what one character can be. - Quantifiers (
? * + {n,m}) say how many. Add?for lazy. - Anchors (
^ $ \b) match positions, not characters.\bis very useful and often forgotten. - Groups:
(...)captures,(?:...)doesn't,(?P<name>...)names. Backreference with\1or\g<name>. - Flags:
IGNORECASE,MULTILINE,DOTALL,VERBOSE. The last one makes complex patterns readable, so use it. re.compilefor busy loops and for giving patterns names.- Always use
r"..."raw strings. - Regex is for patterns, not structure. For HTML, JSON, CSV and code, use real parsers.
Next: Datetime, Timezones, and Friends: handling dates and times correctly, which is harder than it looks.
Practice this
on practicepython.inShort exercises that run in your browser and tell you what your code did, as well as whether a test passed.
Found this useful? Share it with a friend who’s learning Python.