A regular expression is a tiny pattern language for matching text. Python's re module finds, extracts, validates, splits and rewrites strings with it.
The five ways to match
| Function | Returns | Matches where? |
|---|---|---|
re.match(p, s) | Match or None | Only at the start of s |
re.search(p, s) | Match or None | The first place anywhere in s |
re.fullmatch(p, s) | Match or None | The whole string — best for validation |
re.findall(p, s) | list of strings (or tuples of groups) | Every non-overlapping match |
re.finditer(p, s) | Iterator of Match objects | Every match, lazily, with positions |
import re
s = "Order 66 shipped on 2026-09-29"
print(re.match(r"\d+", s)) # None ← s doesn't start with a digit
m = re.search(r"\d+", s)
print(m.group(), m.start(), m.end(), m.span()) # 66 6 8 (6, 8)
print(re.findall(r"\d+", s)) # ['66', '2026', '09', '29']
print(bool(re.fullmatch(r"\d{4}", "2026")), bool(re.fullmatch(r"\d{4}", "20261"))) # True False
print([m.start() for m in re.finditer(r"\d+", s)]) # [6, 20, 25, 28]Pattern syntax cheat sheet
| Pattern | Matches |
|---|---|
. | Any character except newline (any at all with re.DOTALL) |
\d / \D | Digit / non-digit (Unicode digits by default) |
\w / \W | Word char [A-Za-z0-9_] + Unicode letters / anything else |
\s / \S | Whitespace / non-whitespace |
[abc], [a-z], [^0-9] | Character class, range, negated class |
^ / $ | Start / end of string (of each line with re.MULTILINE) |
\A / \Z | Start / end of string, always |
\b / \B | Word boundary / not a boundary |
a|b | Alternation: a or b |
\., \*, \( | Escaped literal metacharacters (or use re.escape(text)) |
| Quantifier | Means | Lazy version |
|---|---|---|
* | 0 or more | *? |
+ | 1 or more | +? |
? | 0 or 1 (optional) | ?? |
{n} | Exactly n | — |
{n,m} | Between n and m | {n,m}? |
{n,} | n or more | {n,}? |
import re
text = "cat catalog concat Cat"
print(re.findall(r"cat", text)) # ['cat', 'cat', 'cat']
print(re.findall(r"\bcat\b", text)) # ['cat']
print(re.findall(r"\bcat\b", text, re.I)) # ['cat', 'Cat']
print(re.findall(r"colou?r", "color colour")) # ['color', 'colour']
print(re.findall(r"[A-Z][a-z]+", "Asha met Ravi in Pune")) # ['Asha', 'Ravi', 'Pune']
print(re.findall(r"\d{2,3}", "1 12 123 1234")) # ['12', '123', '123']
print(re.escape("1+1=2?")) # 1\+1=2\?Groups: capture, name, skip
import re
m = re.search(r"(\d{4})-(\d{2})-(\d{2})", "due 2026-09-29!")
print(m.group(0), m.group(1), m.groups()) # 2026-09-29 2026 ('2026', '09', '29')
m = re.search(r"(?P<user>[\w.]+)@(?P<domain>[\w.]+)", "mail asha.k@lvn.io now")
print(m["user"], m.group("domain")) # asha.k lvn.io
print(m.groupdict()) # {'user': 'asha.k', 'domain': 'lvn.io'}
print(re.findall(r"(\w+)=(\d+)", "a=1, b=22")) # [('a', '1'), ('b', '22')]
print(re.findall(r"(?:ab)+", "ababab xab")) # ['ababab', 'ab']
print(re.findall(r"(ab)+", "ababab xab")) # ['ab', 'ab'] ← only the last repetition| Syntax | Meaning |
|---|---|
(…) | Capturing group — m.group(1) |
(?P<name>…) | Named group — m['name'], m.groupdict() |
(?:…) | Non-capturing group — groups for | or quantifiers without capturing |
\1, (?P=name) | Backreference to what a group matched |
Backreferences
import re
print(re.findall(r"\b(\w+) \1\b", "this is is a test test")) # ['is', 'test']
print(bool(re.fullmatch(r"(['\"]).*\1", "'quoted'"))) # True
print(re.sub(r"\b(\w+) \1\b", r"\1", "the the cat sat sat")) # the cat satSubstitute & split
import re
print(re.sub(r"\s+", " ", "too many\n spaces")) # too many spaces
print(re.sub(r"(\d{4})-(\d{2})-(\d{2})", r"\3/\2/\1", "2026-09-29")) # 29/09/2026
print(re.sub(r"(?P<y>\d{4})", r"<\g<y>>", "in 2026")) # in <2026>
print(re.sub(r"\d+", lambda m: str(int(m.group()) * 2), "3 apples, 10 pears")) # 6 apples, 20 pears
print(re.subn(r"a", "4", "banana")) # ('b4n4n4', 3)
print(re.split(r"[,;]\s*", "a, b;c; d")) # ['a', 'b', 'c', 'd']
print(re.split(r"([,;])", "a,b;c")) # ['a', ',', 'b', ';', 'c']Flags
| Flag | Inline | Effect |
|---|---|---|
re.IGNORECASE / re.I | (?i) | Case-insensitive |
re.MULTILINE / re.M | (?m) | ^ and $ match at each line |
re.DOTALL / re.S | (?s) | . also matches newline |
re.VERBOSE / re.X | (?x) | Ignore whitespace, allow # comments in the pattern |
re.ASCII / re.A | (?a) | \w, \d, \s match ASCII only |
import re
log = "ERROR disk\nINFO ok\nERROR net"
print(re.findall(r"^ERROR (\w+)", log)) # ['disk']
print(re.findall(r"^ERROR (\w+)", log, re.M)) # ['disk', 'net']
print(re.search(r"disk.INFO", log)) # None
print(re.search(r"disk.INFO", log, re.S).group() == "disk\nINFO") # True
phone = re.compile(r"""
(\d{3}) # area code
[-.\s]? # optional separator
(\d{4}) # number
""", re.VERBOSE)
print(phone.search("call 080-4455").groups()) # ('080', '4455')Greedy vs lazy
import re
html = "<b>bold</b> and <i>italic</i>"
print(re.findall(r"<.+>", html)) # ['<b>bold</b> and <i>italic</i>'] ← greedy: as much as possible
print(re.findall(r"<.+?>", html)) # ['<b>', '</b>', '<i>', '</i>'] ← lazy: as little as possible
print(re.findall(r"<[^>]+>", html)) # ['<b>', '</b>', '<i>', '</i>'] ← negated class: fastest & clearestLookahead & lookbehind
Lookarounds check what is next to the current position without consuming it — they are zero-width, so they don't appear in the match.
| Syntax | Name | Asserts that… |
|---|---|---|
X(?=Y) | Positive lookahead | Y follows |
X(?!Y) | Negative lookahead | Y does not follow |
(?<=Y)X | Positive lookbehind | Y precedes (fixed width) |
(?<!Y)X | Negative lookbehind | Y does not precede |
import re
print(re.findall(r"\d+(?=%)", "5% of 200 is 10")) # ['5']
print(re.findall(r"(?<=₹)\d+", "₹250 or $30")) # ['250']
print(re.findall(r"\b\d+\b(?!%)", "5% of 200 is 10")) # ['200', '10']
print(re.sub(r"\B(?=(\d{3})+(?!\d))", ",", "1234567")) # 1,234,567
strong = re.compile(r"(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}")
print(bool(strong.fullmatch("Passw0rdX")), bool(strong.fullmatch("password1"))) # True FalseCompile & reuse
import re
DATE = re.compile(r"(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})")
rows = ["2026-01-05 login", "bad row", "2026-02-11 logout"]
dates = [m["m"] for r in rows if (m := DATE.search(r))]
print(dates) # ['01', '02']
print(DATE.pattern[:6], DATE.groups) # (?P<y> 3Practical recipes
import re
EMAIL = re.compile(r"[\w.+-]+@[\w-]+(\.[\w-]+)+") # good enough for forms, not RFC 5322
print(bool(EMAIL.fullmatch("a.b+tag@mail.co.in")), bool(EMAIL.fullmatch("a@b"))) # True False
tokens = re.findall(r"\d+\.?\d*|[-+*/()]", "3.5*(2+10)/4")
print(tokens) # ['3.5', '*', '(', '2', '+', '10', ')', '/', '4']
def snake(name):
return re.sub(r"(?<!^)(?=[A-Z])", "_", name).lower()
print(snake("parseHTTPResponseCode")) # parse_h_t_t_p_response_code
print(re.sub(r"([a-z0-9])([A-Z])", r"\1_\2", "userIdCount").lower()) # user_id_count
print(re.findall(r"#(\w+)", "loving #python and #regex!")) # ['python', 'regex']Performance & when not to use regex
Interview questions
re.match vs re.search vs re.fullmatch?
What's the difference between greedy and lazy quantifiers?
Why use raw strings for regex?
Extract all numbers (including decimals and negatives) from a string.