Skip to content

Regular Expressions Interview Questions & Answers

15 questions Updated 2026-06-18 Share:

Python interview questions on the re module — match vs search vs fullmatch, capture and named groups, re.compile, greedy vs non-greedy, re.sub, and raw strings.

Read the in-depth guidePython Regular Expressions Explained — The re Module, Groups, and Common Patterns(opens in new tab)
15 of 15

They differ in where the pattern must match. re.match anchors at the start of the string (but not the end). re.search scans for the pattern anywhere in the string. re.fullmatch requires the pattern to match the entire string. All return a Match object on success or None on failure.

import re
re.match("ab", "abcd")      # match — starts with 'ab'
re.match("cd", "abcd")      # None  — not at the start
re.search("cd", "abcd")     # match — found anywhere
re.fullmatch("ab", "abcd")  # None  — must match the whole string
re.fullmatch("abcd", "abcd")# match

A common bug is using match expecting whole-string validation — it only anchors the start. Rule of thumb: use search to find, fullmatch to validate, and match only when you specifically mean "begins with".

Parentheses ( ) create a capture group you retrieve by number (1-based; group 0 is the whole match). (?P<name>...) creates a named group you retrieve by name — far more readable. (?:...) groups without capturing when you only need it for grouping/alternation.

import re
m = re.search(r"(\d{4})-(\d{2})", "2026-06")
m.group(0)   # '2026-06'  — whole match
m.group(1)   # '2026'     — first group
m.groups()   # ('2026', '06')

m = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})", "2026-06")
m.group("year")   # '2026'
m.groupdict()     # {'year': '2026', 'month': '06'}

Named groups make patterns self-documenting and resilient to reordering. Rule of thumb: use (?P<name>...) for anything you'll extract, and (?:...) when grouping is structural only.

re.compile(pattern) builds a reusable pattern object once, then you call methods (.search, .match, .findall, .sub) on it. The module-level functions actually compile internally and cache recent patterns, so the main win is clarity and reuse — plus a small speedup when a pattern is used many times in a loop.

import re
DATE = re.compile(r"(?P<year>\d{4})-(?P<month>\d{2})")  # compile once

for line in lines:
    m = DATE.search(line)   # reuse the compiled object
    if m:
        print(m.group("year"))

It also lets you attach flags (e.g. re.IGNORECASE, re.VERBOSE) in one place. Rule of thumb: compile patterns used repeatedly or shared across a module; for one-off use the module functions are fine.

By default quantifiers (*, +, ?, {m,n}) are greedy — they match as much as possible, then backtrack. Adding a trailing ? makes them non-greedy (lazy) — they match as little as possible. This matters hugely when a delimiter can appear multiple times.

import re
text = "<a><b>"
re.search(r"<.*>", text).group()    # '<a><b>'  — greedy, grabs everything
re.search(r"<.*?>", text).group()   # '<a>'     — lazy, stops at first '>'

Greedy patterns over-matching is a classic "regex ate too much" bug. Rule of thumb: when matching content between delimiters, reach for the lazy *? / +? (or a negated character class like [^>]*).

re.sub(pattern, repl, string) returns a new string with all matches replaced. The replacement can reference captured groups with \1 or \g<name>, or be a function that receives each Match for dynamic replacement. You should write patterns as raw strings (r"...") so that backslash escapes like \d and \b reach the regex engine instead of being interpreted by Python first.

import re
re.sub(r"\s+", " ", "a   b\tc")          # 'a b c'  — collapse whitespace
re.sub(r"(\d{4})-(\d{2})", r"\2/\1", "2026-06")  # '06/2026' — reorder groups
re.sub(r"\d+", lambda m: f"[{m.group()}]", "x9")  # 'x[9]' — function repl

"\d"     # in a normal string this is an invalid escape (warns)
r"\d"    # raw string — passes \d straight to the engine

Without r"", "\b" becomes a backspace character, not a word boundary — a subtle, hard-to-spot bug. Rule of thumb: always prefix regex patterns with r.

findall returns a list of matches (strings, or tuples if there are multiple groups). finditer returns a lazy iterator of Match objects, giving you positions and group access — better for large inputs or when you need match details.

import re
re.findall(r"\d+", "a1b22c333")          # ['1', '22', '333']
re.findall(r"(\w)(\d)", "a1b2")          # [('a','1'), ('b','2')] — tuples!

for m in re.finditer(r"\d+", "a1b22"):
    print(m.group(), m.start(), m.end())  # value + position

Rule of thumb: findall for a quick list of values; finditer when you need spans, groups per match, or memory-efficient iteration. Watch findall's tuple behavior with multiple groups.

re.IGNORECASE case-insensitive; re.MULTILINE makes ^/$ match at each line; re.DOTALL lets . match newlines; re.VERBOSE allows whitespace and comments in the pattern. Combine with |.

import re
re.findall(r"^\w+", "foo\nbar", re.MULTILINE)   # ['foo', 'bar']
re.search(r"a.b", "a\nb", re.DOTALL)            # matches across newline
re.findall(r"cat", "Cat CAT", re.IGNORECASE)    # ['Cat', 'CAT']

pat = re.compile(r"""
    \d{4}   # year
    -\d{2}  # month
""", re.VERBOSE)

Rule of thumb: MULTILINE for line-based ^/$, DOTALL for .-spans-newlines, VERBOSE for readable complex patterns; combine via re.A | re.B.

^ matches start of string (or line in MULTILINE), $ matches end. \b is a word boundary (between word/non-word chars); \B is a non-boundary. Anchors match positions, not characters.

import re
re.search(r"\bcat\b", "the cat sat")     # matches 'cat' as a whole word
re.search(r"\bcat\b", "category")        # None — 'cat' not a full word
re.findall(r"^\d", "5x", )               # ['5'] — at start
re.sub(r"\s+$", "", "trim   ")           # 'trim' — trailing whitespace

Rule of thumb: use \b to match whole words (avoiding partial matches), and ^/$ to anchor to string/line edges.

A backreference (\1, or (?P=name)) matches the same text a prior group captured — useful for finding repeats or matched pairs. They make the regex context-sensitive within a single match.

import re
re.search(r"(\w+) \1", "the the end")        # matches 'the the' — repeated word
re.search(r"<(\w+)>.*</\1>", "<b>hi</b>")    # matched open/close tag
re.search(r"(?P<q>['\"]).*?(?P=q)", "'hi'")  # same quote char on both ends

Rule of thumb: backreferences match previously-captured text (\1/(?P=name)) — ideal for duplicate detection and matching paired delimiters.

re.split(pattern, s) splits on a regex, not a fixed substring — so you can split on variable delimiters. If the pattern has capturing groups, the delimiters are included in the result.

import re
re.split(r"\s*,\s*", "a, b ,c")        # ['a', 'b', 'c'] — flexible spacing
re.split(r"[;,]", "a,b;c")             # ['a', 'b', 'c'] — multiple delimiters
re.split(r"(\d)", "a1b2")              # ['a', '1', 'b', '2', ''] — keeps captures

Rule of thumb: use re.split for variable/multi-character delimiters; capturing groups in the pattern keep the separators in the output.

Nested or overlapping quantifiers (e.g. (a+)+) can make the engine try an exponential number of paths on non-matching input — freezing your program (a ReDoS risk). Avoid ambiguous nesting and prefer specific character classes.

import re
# DANGEROUS: (a+)+ on "aaaaaaaaaaaaaaaaX" backtracks catastrophically
# re.match(r"(a+)+$", "a" * 30 + "X")   # may hang

# safer: unambiguous, no nested quantifier
re.match(r"a+$", "a" * 30 + "X")        # fails fast

Rule of thumb: avoid nested quantifiers over overlapping patterns; use atomic groups/ possessive quantifiers (3.11+ (?>...), a++) or precise classes to prevent ReDoS.

Lookarounds match a position based on what follows/precedes without consuming characters: (?=...) positive lookahead, (?!...) negative lookahead, (?<=...) positive lookbehind, (?<!...) negative lookbehind.

import re
re.findall(r"\d+(?= dollars)", "5 dollars 10 euros")   # ['5'] — followed by 'dollars'
re.findall(r"(?<=\$)\d+", "$5 and $10")                # ['5', '10'] — preceded by $
re.sub(r"(?<!^)(?=(\d{3})+$)", ",", "1234567")          # '1,234,567' — thousands

Rule of thumb: use lookarounds to assert context (what's around a match) without including it in the result; lookbehind must be fixed-width in Python's re.

A Match exposes .group()/.groups()/.groupdict() for captured text, and .start()/.end()/.span() for positions. .group(0) is the whole match. These let you locate and extract in one pass.

import re
m = re.search(r"(\w+)@(\w+)", "to bob@acme now")
m.group()        # 'bob@acme'
m.groups()       # ('bob', 'acme')
m.start(), m.end()   # (3, 11)
m.span(1)        # (3, 6) — span of group 1

Rule of thumb: a Match carries both the captured text (groups) and where it occurred (span/start/end) — use it instead of re-searching for positions.

re.sub(pat, repl, s, count=N) replaces only the first N matches. re.subn returns a tuple (new_string, number_of_substitutions) so you know how many replacements happened.

import re
re.sub(r"o", "0", "foo boo", count=1)    # 'f0o boo' — only first
re.subn(r"o", "0", "foo boo")            # ('f00 b00', 4)

new, n = re.subn(r"\bthe\b", "a", "the cat the dog")
n                                         # 2

Rule of thumb: use count= to cap replacements and subn when you need the replacement count (e.g. to detect whether anything changed).

Use re.escape() to backslash-escape all special characters, so user input or a variable is matched literally rather than interpreted as a pattern — preventing both bugs and injection.

import re
user = "a.b+c"
re.search(re.escape(user), "xa.b+cy")    # matches the literal 'a.b+c'
# without escape, '.' and '+' would be metacharacters

pattern = re.compile(re.escape(delimiter))

Rule of thumb: wrap any dynamic/literal text in re.escape() before embedding it in a pattern — never trust raw input to be regex-safe.

More ways to practice

The self-quiz is live. Join our channel for updates, new content & tech tips.

Join our WhatsApp Channel