All posts

Regex differences: JavaScript, Python and Java

Named groups, lookbehind, flags, string escaping, match vs fullmatch and Unicode classes compared across JavaScript, Python and Java, with tested examples.

By Last reviewed 6 min readRegexJavaScriptPythonJava

A pattern that works in the browser can fail to compile in Python, compile in Java and match something different, or pass every test except the one input with a trailing newline. The syntax overlaps enough that the differences catch you off guard. This guide covers the ones that cause real bugs, each checked on Node 24, CPython 3.12 and 3.14, and OpenJDK 21, with notes where older versions differ.

The regex tester runs your pattern on your browser's own JavaScript engine, so what it shows is JavaScript behaviour. Get the pattern right there, then translate it with the notes below before it goes into Python or Java.

The short version

                       JavaScript          Python re            Java
named group            (?<id>…)            (?P<id>…)            (?<id>…)
named backreference    \k<id>              (?P=id)              \k<id>
named in replacement   $<id>               \g<id>               ${id}
lookbehind             any pattern         fixed width          bounded length
whole-string match     ^(?:…)$             re.fullmatch()       matcher.matches()
$ before a final \n    no                  yes                  yes
\d and \w              ASCII               Unicode              ASCII by default
\p{L}                  with u or v flag    not in re            yes
atomic, possessive     no                  3.11 and later       yes
inline (?i)            (?i:…) only, ES2025 start of pattern     anywhere

Named groups and backreferences

JavaScript (since ES2018) and Java write a named group as (?<name>…). Python's re module uses (?P<name>…) and rejects the other spelling: as of Python 3.14, re.compile(r"(?<y>\d{4})") fails with unknown extension ?<y. Java and JavaScript reject the P form in turn.

// JavaScript
const m = /(?<year>\d{4})-(?<month>\d{2})/.exec("2026-10");
m.groups.year                                                     // "2026"
"2026-10".replace(/(?<y>\d{4})-(?<m>\d{2})/, "$<m>/$<y>")         // "10/2026"

# Python
m = re.search(r"(?P<year>\d{4})-(?P<month>\d{2})", "2026-10")
m.group("year")                                                   # '2026', also m["year"]
re.sub(r"(?P<y>\d{4})-(?P<m>\d{2})", r"\g<m>/\g<y>", "2026-10")   # '10/2026'

// Java
Matcher m = Pattern.compile("(?<year>\\d{4})-(?<month>\\d{2})").matcher("2026-10");
if (m.find()) m.group("year");                                    // "2026"
"2026-10".replaceAll("(?<y>\\d{4})-(?<m>\\d{2})", "${m}/${y}")   // "10/2026"

Two smaller traps. Java group names must start with a letter and may contain only ASCII letters and digits, so (?<order_id>…) is a syntax error in Java but fine in JavaScript and Python. And JavaScript (ES2025) lets one name appear in different alternatives, as in (?<y>\d{4})-\d\d|\d\d-(?<y>\d{4}); Python and Java both reject the duplicate.

Lookbehind

All three support (?<=…) and (?<!…). They disagree on what can go inside.

  • JavaScript accepts any pattern, including unbounded ones like (?<=^\w+:)\d+. Lookbehind is part of ES2018, but Safari only added it in 16.4, which matters if your page has to work on older iPhones.
  • Python requires a fixed width. (?<=\$|USD )\d+ fails with look-behind requires fixed-width pattern, because the alternatives are one and four characters long. Alternatives of equal length are fine, and unequal ones can be split into separate lookbehinds: (?:(?<=\$)|(?<=USD ))\d+.
  • Java requires a computable maximum length. (?<=\$\d{1,3}) and (?<=\$|USD ) both work. A repeated group such as (?<=(ab)+) fails with "Look-behind group does not have an obvious maximum length". JDK 21 also accepts + or * on a single character class, like (?<=\d+), but explicit bounds are the safer habit.

When a lookbehind won't port, capture instead. (?:\$|USD )(\d+) works in all three engines; read group 1.

Flags

JavaScript flags follow the literal or go in the second argument of new RegExp: g (all matches), i, m, s (dot matches newline), u (Unicode mode), v (Unicode sets, ES2024), y (sticky: match only at lastIndex) and d (match indices). There's no x flag for comments; new RegExp("a b", "x") throws. A global (?i) at the start of a pattern is a syntax error, but ES2025 added scoped modifiers like (?i:abc); current V8 accepts them, so check your other target browsers before relying on them. The tester has toggles for the five you'll use most: g, i, m, s and u.

Python has no global flag; you pick a function instead. re.search finds the first match, re.findall and re.finditer find all of them. The other flags are constants combined with |: re.I, re.M, re.S, re.X (verbose: whitespace ignored, # starts a comment) and re.A (ASCII-only classes). An inline (?i) must sit at the very start of the pattern; since Python 3.11, putting it anywhere else is an error. The scoped form (?i:…) works anywhere.

Watch re.findall on a pattern with groups: it returns the groups, not the whole matches. re.findall(r"(\w+)=(\d+)", "a=1 b=2") gives [('a', '1'), ('b', '2')].

Java takes a bit mask in Pattern.compile: Pattern.CASE_INSENSITIVE, MULTILINE, DOTALL, COMMENTS, UNICODE_CASE and UNICODE_CHARACTER_CLASS, among others. Inline flags work anywhere and apply from that point on, so ab(?i)c matches abC.

Escaping: the string literal goes first

Before the regex engine sees your pattern, the language's string parser does, and each language fails differently when you forget:

// JavaScript: a regex literal needs no extra escaping
/\d+/
new RegExp("\\d+")            // in a string, write \\ for each \
new RegExp("\d+")             // silently becomes /d+/: matches "ddd", not "42"
new RegExp(String.raw`\d+`)   // String.raw keeps backslashes as typed

# Python: use raw strings
re.search(r"\bcat\b", "a cat here")   # matches "cat"
re.search("\bcat\b", "a cat here")    # None: "\b" here is a backspace character

// Java: no raw strings, so double every backslash
Pattern.compile("\\d+");
Pattern.compile("\d+");               // compile error: illegal escape character
Pattern.compile("\bcat\b");           // compiles, but \b is a backspace and never matches

Python lets "\d" through unchanged because it isn't a recognised string escape, so it works by accident; since 3.12 it at least emits a SyntaxWarning. Java text blocks don't help, because they process escapes like ordinary strings. To match a literal piece of text inside a pattern, use re.escape() in Python and Pattern.quote() in Java.

Moving a pattern from the tester into a Java or JavaScript string by hand is where backslashes go missing. The string escape tool in Java/C mode doubles every backslash for you.

match, search and fullmatch

The method you call decides how much of the string has to match, and the names don't line up across languages:

              anywhere             at the start          whole string
JavaScript    re.test(s)           ^ in the pattern      ^(?:…)$ in the pattern
Python        re.search()          re.match()            re.fullmatch()
Java          matcher.find()       matcher.lookingAt()   matcher.matches()

Two of these cause most of the bugs. Python's re.match sounds like "match the whole thing" but only anchors the start, so re.match(r"\d+", "42a") succeeds. Java's String.matches() goes the other way and requires the whole string, so "hotdog".matches("cat|dog") is false even though find() succeeds.

Then there's the trailing newline. In Python and Java, $ matches at the end of the string and just before a final \n, so ^\d+$ accepts "42\n". In JavaScript it doesn't. When you validate input that may carry a newline, such as a line read from a file, use re.fullmatch() or matcher.matches(), or anchor the end with \Z in Python and \z in Java. Python 3.14 also accepts \z.

Unicode: what \d, \w and the dot mean

\b\w+\b on "café bar"

JavaScript, with or without u    caf, bar
Python 3                         café, bar
Java 21, default flags           caf, bar
Java 21 with (?U)                café, bar
  • JavaScript: \d, \w and \b are ASCII-only, even with u. For letters in any script, use \p{L}, which needs the u or v flag. Without u the engine works on UTF-16 code units, so ^.$ fails on a single character outside the Basic Multilingual Plane, which includes most emoji.
  • Python 3: str patterns are Unicode-aware by default. \d matches the Arabic-Indic digit ٢ as well as 2, which may not be what a "digits only" field expects. Use re.ASCII or an explicit [0-9] to restrict it. The standard re module has no \p{…} classes; the third-party regex package is a separate engine that adds them.
  • Java: \d and \w are ASCII by default, and UNICODE_CHARACTER_CLASS or inline (?U) makes them Unicode-aware. \p{L} works without any flag. CASE_INSENSITIVE on its own only folds ASCII, so é doesn't match É until you add UNICODE_CASE.

Next steps

Build and debug the pattern in the regex tester, then run it past the table at the top before it moves to Python or Java. The language notes at the bottom of the regex cheat sheet are a quick reminder, and the capture groups guide explains group numbering and replacements in detail.

Paste anything. devprix picks the tool.

Press ⌘K on any page, paste JSON, a JWT, a timestamp or a URL, and the right tool opens with your data filled in.