Regex Tester Python: Test Python Regular Expressions Online

Python's `re` module shares the core syntax of most flavours, so an online regex tester gets you most of the way. The rest — raw strings, how `match` differs from `search`, Python's flag names and the `re.sub` replacement syntax — is exactly where a pattern that worked in the tester stops working in code. These are the differences worth knowing before you paste.

Always use raw strings

Write patterns as r"\d+", never "\d+". Without the r prefix Python processes backslash escapes first, so the regex engine receives something other than what you typed.

"\d"    # SyntaxWarning (Python 3.12+), then passes '\d' through
"\b"    # becomes a backspace character (0x08) — silently wrong
r"\b"   # stays as the two characters \ and b — a word boundary

"\\b"   # also correct, but harder to read

\b is the dangerous one because it fails silently. In a normal string it becomes an ASCII backspace, so the pattern compiles, runs, and simply never matches. Nothing warns you.

This is the single most common reason a pattern copied out of a tester behaves differently in Python. The tester shows the pattern itself; the raw-string prefix is how you preserve it through Python's string literal parsing.

The tester on this site runs JavaScript's engine, which accepts most Python patterns unchanged. Two things do not carry over. (?P<name>…) is an Invalid group error in JavaScript, so test with (?<name>…) and add the P back in Python. And \A and \Z are not anchors in JavaScript: without the u flag, \A silently matches a literal A. Use ^ and $ in the tester and switch back when you paste the pattern into Python.

match, search, fullmatch and findall

Python splits into four functions where many languages have one, and picking the wrong one accounts for most of the remaining surprises.

import re

re.match(r"\d+", "abc 123")      # None  — anchored at the start
re.search(r"\d+", "abc 123")     # <Match '123'>  — scans
re.fullmatch(r"\d+", "123abc")   # None  — must match entirely
re.findall(r"\d+", "a1 b22 c3")  # ['1', '22', '3']  — strings
re.finditer(r"\d+", "a1 b22")    # match objects with positions

re.match anchors at the beginning of the string. It does not mean *does this match anywhere* — that is re.search, and search is what an online tester shows you by default. A pattern that highlights correctly in a tester and returns None from re.match is almost always this.

re.fullmatch is usually what you want for validation, because it removes the need to wrap the pattern in ^...$ and cannot be defeated by a newline the way $ can.

re.findall has a behaviour worth memorising: with no capture group it returns whole matches, with one group it returns that group, and with several it returns tuples. Adding a group to an existing pattern therefore changes the return type — a common source of breakage when a pattern is edited later.

re.findall(r"\d+-\d+", "10-20")        # ['10-20']
re.findall(r"(\d+)-\d+", "10-20")      # ['10']
re.findall(r"(\d+)-(\d+)", "10-20")    # [('10', '20')]

Python's flags

The flag names differ from the single letters most testers use, and two of them behave in ways worth knowing.

re.IGNORECASE  re.I    case-insensitive
re.MULTILINE   re.M    ^ and $ match at line boundaries
re.DOTALL      re.S    . also matches newline
re.VERBOSE     re.X    ignore whitespace, allow # comments
re.ASCII       re.A    \w \d \b become ASCII-only

re.search(pattern, text, re.I | re.M)   # combine with |

re.S does the same as the s flag in JavaScript: . also matches \n. The confusing part is the name. Perl, PCRE and .NET call this mode "single-line" (RegexOptions.Singleline in C#), which sounds like the opposite of re.MULTILINE but has nothing to do with it: re.M changes ^ and $, re.S changes the dot.

re.ASCII matters more than it looks. By default Python 3 makes \w, \d and \b Unicode-aware, so \d matches Arabic-Indic and Devanagari digits, and \w matches accented letters. That is usually what you want for text and definitely not what you want for validating an identifier or parsing a numeric field.

re.fullmatch(r"\d+", "١٢٣")          # matches — Arabic-Indic digits
re.fullmatch(r"\d+", "١٢٣", re.A)    # None
re.fullmatch(r"[0-9]+", "١٢٣")       # None — explicit is safer

re.VERBOSE is Python's best feature here. It ignores whitespace and allows # comments inside the pattern, which turns an unreadable one-liner into something a reviewer can check.

pattern = re.compile(r"""
    (?P<year>\d{4}) -     # four-digit year
    (?P<month>\d{2}) -    # month
    (?P<day>\d{2})        # day
""", re.VERBOSE)

In verbose mode a literal space must be escaped as \ or written as [ ], since unescaped whitespace is now ignored.

Flags can also be set inline at the start of a pattern — (?i) or (?is) — which is how you carry them across from a tester that accepts only a pattern string with no flag field.

Named groups and re.sub replacements

Python names groups with (?P<name>...). The P is required and is Python-specific; the (?<name>...) form used by JavaScript and .NET is a syntax error here.

m = re.search(r"(?P<user>\w+)@(?P<host>[\w.]+)", "ada@example.com")
m.group("user")   # 'ada'
m.groupdict()     # {'user': 'ada', 'host': 'example.com'}
m.span("host")    # (4, 15)

Substitution uses backslash references, not dollar signs. A replacement string copied from JavaScript inserts a literal $1 instead of the captured text — and because that is a valid string, nothing raises an error.

re.sub(r"(\d+)-(\d+)", r"\2-\1", "10-20")          # '20-10'
re.sub(r"(?P<a>\d+)", r"[\g<a>]", "42")            # '[42]'
re.sub(r"(\d+)", "$1", "42")                       # '$1'  — wrong

re.sub also accepts a function as the replacement, which is far clearer than a dense pattern when the transformation has conditions.

re.sub(r"\d+", lambda m: str(int(m.group()) * 2), "a1 b2")
# 'a2 b4'

Compiling, and when it matters

re.compile returns a reusable pattern object. Python caches compiled patterns internally, so the performance argument is weaker than it is often stated — the cache holds 512 entries and covers most programs.

EMAIL = re.compile(r"[^@\s]+@[^@\s]+\.\w+")

if EMAIL.fullmatch(value):
    ...

Compiling is still worth it for two reasons that have nothing to do with speed: it names the pattern, and it puts it at module level where it is visible in review rather than buried in a loop. The cache is also bypassed when a pattern is built by string interpolation, and there the cost is real.

If you interpolate user input into a pattern, re.escape it. Without that, a value containing ( or [ either raises or silently changes what the pattern matches.

re.search(re.escape(user_input) + r"\d+", text)

Catastrophic backtracking

Python's re uses a backtracking engine with no built-in timeout, so a pattern with nested quantifiers over a non-matching string can hang the process.

re.match(r"(a+)+$", "a" * 30 + "b")   # effectively never returns

The shape to watch for is a quantifier applied to a group that itself contains a quantifier — (a+)+, (\w*\s?)*, (.*,)*. Each additional character roughly doubles the work.

The fixes are to make the inner part more specific so alternatives cannot overlap, to anchor the pattern, or to stop using a regex where a split would do. If you must run untrusted patterns, the third-party regex module supports a timeout; the standard library does not.

Frequently asked questions

Why does my pattern match in the tester but not in Python?

Two causes cover nearly all cases. The pattern was not written as a raw string, so Python consumed the backslashes before the regex engine saw them — `"\b"` becomes a backspace character and never matches. Or the code uses `re.match`, which anchors at the start of the string, where the tester was showing `re.search` behaviour.

What is the difference between re.match, re.search and re.fullmatch?

`re.match` anchors at the beginning and matches a prefix. `re.search` scans the whole string for the first match anywhere. `re.fullmatch` requires the pattern to consume the entire string, which makes it the right choice for validation because it needs no `^...$` wrapping.

How do I make . match a newline in Python?

Pass `re.DOTALL`, also written `re.S`, or set it inline with `(?s)` at the start of the pattern. Perl, PCRE and .NET call the same mode *single-line*, which is unrelated to `re.MULTILINE` despite the name.

Why does re.findall return tuples?

Its return type depends on how many capture groups the pattern has: no groups returns whole matches, one group returns that group, and two or more return tuples. Adding a group to an existing pattern therefore changes the return type. Use `re.finditer` when you want match objects regardless of grouping.

Should I compile my regex patterns?

Python caches compiled patterns automatically, so the speed benefit is small for most code. Compile at module level anyway when a pattern is reused — it gives the pattern a name and makes it visible in review. The cache does not help patterns built by string interpolation, where compiling genuinely matters.

Why does \d match non-ASCII digits?

Python 3 makes `\w`, `\d` and `\b` Unicode-aware by default, so `\d` matches Arabic-Indic and Devanagari digits among others. Pass `re.ASCII` to restrict them, or write `[0-9]` explicitly — the explicit form is clearer and cannot be changed by a flag somewhere else.