Skip to content
  • Regular Expressions (re) · Python

Regular Expressions (re)

LearnViaNotes @learnvianotes· 6 min read

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

FunctionReturnsMatches where?
re.match(p, s)Match or NoneOnly at the start of s
re.search(p, s)Match or NoneThe first place anywhere in s
re.fullmatch(p, s)Match or NoneThe 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 objectsEvery match, lazily, with positions
Python
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

PatternMatches
.Any character except newline (any at all with re.DOTALL)
\d / \DDigit / non-digit (Unicode digits by default)
\w / \WWord char [A-Za-z0-9_] + Unicode letters / anything else
\s / \SWhitespace / non-whitespace
[abc], [a-z], [^0-9]Character class, range, negated class
^ / $Start / end of string (of each line with re.MULTILINE)
\A / \ZStart / end of string, always
\b / \BWord boundary / not a boundary
a|bAlternation: a or b
\., \*, \(Escaped literal metacharacters (or use re.escape(text))
QuantifierMeansLazy 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,}?
Python
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

Python
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
SyntaxMeaning
(…)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

Python
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 sat

Substitute & split

Python
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

FlagInlineEffect
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
Python
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

Python
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 & clearest

Lookahead & 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.

SyntaxNameAsserts that…
X(?=Y)Positive lookaheadY follows
X(?!Y)Negative lookaheadY does not follow
(?<=Y)XPositive lookbehindY precedes (fixed width)
(?<!Y)XNegative lookbehindY does not precede
Python
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 False

Compile & reuse

Python
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> 3

Practical recipes

Python
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.

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report