HSHimanshu Sharma
Python Notes

Python regular expressions: the re module, patterns, groups and flags

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, or None.
  • 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 match when you meant search.
  • Calling .group() on None.
  • Forgetting that findall with groups returns tuples.
  • Using greedy .* and matching too much.
  • Not escaping a dot (example.com also matches exampleXcom).
  • Validating with search instead of fullmatch.

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.

Advertisement