Python regular expressions: the re module, patterns, groups and flags
Advertisement
A regular expression (regex) is a compact pattern that describes a kind of text, such as "five digits, a dash, five digits". Python's re module lets you search, check, extract and replace text with such patterns. They look cryptic at first, but they are made of a few small parts. This post teaches those parts, then the functions that use them, with examples that were run on a real interpreter. The sample text used throughout:
import re
t = "Call 98765-43210 or 91234-56789; mail ada@example.com, bob.smith@mail.co.in. Dates: 2026-10-24 and 1999-01-05."
You can try any pattern live in the Regex Tester; note that it uses the JavaScript flavour, which differs from Python's in small details such as named-group syntax.
Always use raw strings
Regex patterns are full of backslashes, and Python strings treat backslashes as escapes too. Write patterns as raw strings, r"...", so that \d reaches the regex engine untouched. Without the r, "\b" means a backspace character instead of a word boundary.
The building blocks
Literal characters match themselves. Special characters need a backslash to be literal: . ^ $ * + ? { } [ ] \ | ( ).
Character classes match one character from a set:
| Pattern | Matches |
|---|---|
\d / \D |
a digit / a non-digit |
\w / \W |
a letter, digit or underscore / anything else |
\s / \S |
whitespace / non-whitespace |
. |
any character except a newline |
[abc] |
a, b or c |
[a-c] |
a to c |
[^aeiou] |
any character except those |
print(re.findall(r"[^aeiou\s]+", "hello world")) # ['h', 'll', 'w', 'rld']
print(re.findall(r"[a-c]+", "abcabd")) # ['abcab']
Quantifiers say how many times the previous item repeats:
| Pattern | Meaning |
|---|---|
* |
zero or more |
+ |
one or more |
? |
zero or one |
{3} |
exactly 3 |
{2,3} |
2 to 3 |
{2,} |
2 or more |
print(re.findall(r"colou?r", "color colour colouur")) # ['color', 'colour']
print(re.findall(r"\d{2,3}", "1 22 333 4444")) # ['22', '333', '444']
Note the last result: 4444 yields 444 and the leftover 4 is too short to match.
Anchors fix a position: ^ start, $ end, \b word boundary.
print(re.findall(r"^\d{3}$", "123"), re.findall(r"^\d{3}$", "1234")) # ['123'] []
print(re.findall(r"\bcat\b", "cat concat cat.")) # ['cat', 'cat']
Alternation and grouping: cat|dog means either; ( ) groups.
print(re.findall(r"cat|dog", "cat dog bird")) # ['cat', 'dog']
The main functions
search, match and fullmatch
re.search(p, s)finds the first match anywhere and returns a match object, orNone.re.match(p, s)matches only at the start of the string.re.fullmatch(p, s)requires the whole string to match. It is the right tool for validation.
print(re.search(r"\d{4}-\d{2}-\d{2}", t).group(), re.search(r"\d{4}-\d{2}-\d{2}", t).span())
# 2026-10-24 (84, 94)
print(re.match(r"Call", t) is not None, re.match(r"mail", t)) # True None
print(re.fullmatch(r"\d+", "123") is not None, re.fullmatch(r"\d+", "123a")) # True None
Always check for None before calling .group(), or you'll get an AttributeError.
findall and finditer
findall returns a list of all matches (strings), or, if the pattern has groups, a list of tuples of the groups. finditer yields match objects one at a time.
print(re.findall(r"\d{5}-\d{5}", t)) # ['98765-43210', '91234-56789']
print(re.findall(r"[\w.]+@[\w.]+\.\w+", t)) # ['ada@example.com', 'bob.smith@mail.co.in']
print(re.findall(r"(\d{4})-(\d{2})-(\d{2})", t)) # [('2026', '10', '24'), ('1999', '01', '05')]
print([m.group() for m in re.finditer(r"\b\w{5}\b", "hello world python")]) # ['hello', 'world']
sub and subn
re.sub(pattern, replacement, string) replaces matches. The replacement can refer to groups with \1, \2, or be a function.
print(re.sub(r"\d", "#", "a1b22")) # a#b##
print(re.sub(r"(\w+)@(\w+)", r"\2 at \1", "ada@example")) # example at ada
print(re.sub(r"\s+", " ", "a b \n c")) # a b c
print(re.subn(r"a", "A", "banana")) # ('bAnAnA', 3)
print(re.sub(r"(?P<w>\w+)@", lambda m: m.group("w").upper() + "@", "ada@x bob@y")) # ADA@x BOB@y
subn also returns the number of replacements.
split
print(re.split(r"[,;]\s*", "a, b;c,d")) # ['a', 'b', 'c', 'd']
print(re.split(r"(\d)", "a1b2c")) # ['a', '1', 'b', '2', 'c'] (a group keeps the separators)
print(re.split(r"\s+", " hi there ")) # ['', 'hi', 'there', '']
compile
If you reuse a pattern, compile it once:
c = re.compile(r"\b[A-Z][a-z]+\b")
print(c.findall("Ada met Bob in Paris"), c.pattern) # ['Ada', 'Bob', 'Paris'] \b[A-Z][a-z]+\b
Groups
Parentheses capture part of a match. A match object gives access to them:
m = re.search(r"(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})", t)
print(m.group("y"), m.groupdict(), m.groups(), m.group(0), m.start(), m.end())
# 2026 {'y': '2026', 'm': '10', 'd': '24'} ('2026', '10', '24') 2026-10-24 84 94
(?P<name>...) is a named group; group(0) is the whole match. Use (?:...) to group without capturing, which matters because findall returns the captured group instead of the whole match:
print(re.findall(r"(?:ab)+", "ababab"), re.findall(r"(ab)+", "ababab")) # ['ababab'] ['ab']
A group that did not participate gives None: re.match(r"(\d+)?-(\d+)", "-20").groups() is (None, '20').
Greedy and lazy
Quantifiers are greedy by default: they take as much as possible. Add ? to make them lazy.
print(re.findall(r"<.+>", "<b>x</b><i>y</i>")) # ['<b>x</b><i>y</i>']
print(re.findall(r"<.+?>", "<b>x</b><i>y</i>")) # ['<b>', '</b>', '<i>', '</i>']
For HTML-like text, a more exact pattern is <[^>]+>. In fact, don't parse real HTML with regex; use an HTML parser.
Flags
| Flag | Short | Effect |
|---|---|---|
re.IGNORECASE |
re.I |
Ignore case |
re.MULTILINE |
re.M |
^ and $ match at each line |
re.DOTALL |
re.S |
. also matches a newline |
re.VERBOSE |
re.X |
Allow whitespace and comments in the pattern |
print(re.findall(r"a.c", "abc a\nc"), re.findall(r"a.c", "abc a\nc", re.S)) # ['abc'] ['abc', 'a\nc']
print(re.findall(r"^\w+", "one\ntwo", re.M), re.findall(r"hello", "Hello HELLO", re.I))
# ['one', 'two'] ['Hello', 'HELLO']
Lookarounds
A lookaround checks the context without consuming it:
print(re.findall(r"(?<=\$)\d+", "cost $50 and $7")) # ['50', '7'] (preceded by $)
print(re.findall(r"\d+(?= dollars)", "5 dollars 6 euros")) # ['5'] (followed by " dollars")
print(re.sub(r"(?<=\d)(?=(\d{3})+$)", ",", "1234567")) # 1,234,567
Escaping user text
If you search for text typed by a user, escape it so that its characters are not treated as pattern syntax:
print(re.escape("a.b*c"), re.findall(re.escape("a.b"), "a.b axb")) # a\.b\*c ['a.b']
Patterns for real tasks
print(bool(re.fullmatch(r"[6-9]\d{9}", "9876543210"))) # True (Indian mobile number shape)
print(bool(re.fullmatch(r"\d{6}", "411001"))) # True (PIN code shape)
print(bool(re.fullmatch(r"[A-Z]{5}\d{4}[A-Z]", "ABCDE1234F"))) # True (PAN shape)
html = '<a href="/a">A</a><a href="/b">B</a>'
print(re.findall(r'href="([^"]+)"', html)) # ['/a', '/b']
log = "ERROR 12:01 disk full\nINFO 12:02 ok\nERROR 12:05 net down"
print(re.findall(r"^ERROR (\d\d:\d\d) (.*)$", log, re.M)) # [('12:01', 'disk full'), ('12:05', 'net down')]
These check the shape of the text, not that it is real: a PIN code can match and still not exist. Validate meaning separately.
Errors
A bad pattern raises re.error when it is compiled:
try:
re.compile("(")
except re.error as e:
print("re.error:", e) # re.error: missing ), unterminated subpattern at position 0
Searching for something that isn't there gives an empty list (findall) or None (search).
Web scraping with regex: a warning
Extracting a few values from a page, such as links or prices, works with regex. For anything bigger, use an HTML parser; regular expressions can't reliably handle nested tags or varied markup. Respect a site's terms and robots rules whenever you fetch pages.
Performance and safety
Nested quantifiers like (a+)+ can take exponential time on some inputs, a problem called catastrophic backtracking. Avoid it, use specific character classes instead of .*, and be careful with patterns applied to untrusted input.
Common mistakes
- Forgetting the raw-string prefix
r. - Using
matchwhen you meantsearch. - Calling
.group()onNone. - Forgetting that
findallwith groups returns tuples. - Using greedy
.*and matching too much. - Not escaping a dot (
example.comalso matchesexampleXcom). - Validating with
searchinstead offullmatch.
Watch and practise
Watch the video lessons: Episode 70: Regular Expressions, Part 1 and Episode 71: Regular Expressions, Part 2. Test patterns in the Regex Tester, review the strings post, and see the general introduction in Regular expressions for beginners.