本页面有中文版。
Cette page est disponible en français.
Esta página está disponible en español.
Guides1 live hunt inside

JavaScript vs Python regex: the differences that bite

The same pattern can behave differently in JavaScript and Python. The differences that cause real bugs: re.match vs test, $ and trailing newlines, Unicode \d, findall and groups, replace vs sub, lookbehind width, named group syntax and inline flags. Every claim checked in Node 23 and Python 3.12.

Regex Hunter logo

· 5 min read

/(?<=\$)\d+(\.\d\d)?/

match

  • $5
  • cost $12.50
  • $0.99
  • pay $100

spare

  • 5
  • 5$
  • USD 12
  • $.50

Most regex syntax is shared between JavaScript and Python: classes, quantifiers, groups, anchors, lookarounds. That's what makes the differences dangerous. A pattern copied from one to the other usually compiles, and then behaves slightly differently.

Here are the differences that cause real bugs, roughly in order of how often they bite. Every behaviour below was checked in Node 23 and Python 3.12. The live examples on this page run in your browser, so they show JavaScript's behaviour.

01re.match only looks at the start

JavaScript's test() and match() search the whole string. Python has three functions, and the one called match only matches at the start:

import re

re.match(r"\d", "a1")      # None: the string doesn't start with a digit
re.search(r"\d", "a1")     # matches "1"
re.fullmatch(r"\d", "1")   # the whole string must match
/\d/.test("a1");  // true: test() searches

So a JavaScript test() is a Python search(). And for validation in Python, re.fullmatch is usually what you want: no anchors to forget.

02$ and a trailing newline

In Python, $ matches at the very end and just before a final newline. JavaScript's $ (without the m flag) only matches at the very end:

re.match(r"^\d{5}$", "90210\n")   # matches in Python
/^\d{5}$/.test("90210\n");        // false in JavaScript

In Python, re.fullmatch or \Z closes the gap. The post on anchors and multiline mode covers this in detail.

03\d and \w in Python are Unicode

In JavaScript, \d is always [0-9], even with the u flag. In Python 3, on a str pattern, \d matches any Unicode digit and \w any letter in any script:

re.fullmatch(r"\d+", "٣٤٥")            # matches in Python (Arabic-Indic digits)
re.fullmatch(r"\d+", "٣٤٥", re.ASCII)  # None
/^\d+$/.test("٣٤٥");   // false
/^\d+$/u.test("٣٤٥");  // still false

When the digits are headed for int() or a database, use [0-9] or re.ASCII in Python.

04findall returns groups, not matches

If a pattern has capture groups, Python's findall returns the groups, not the whole matches:

re.findall(r"\w=\d", "a=1 b=2")      # ['a=1', 'b=2']
re.findall(r"(\w)=(\d)", "a=1 b=2")  # [('a', '1'), ('b', '2')]

JavaScript's match with the g flag always returns whole matches, whatever groups the pattern has:

"a=1 b=2".match(/(\w)=(\d)/g);  // ["a=1", "b=2"]

Adding a group "just for grouping" to a Python pattern silently changes what findall returns. Use a non-capturing group (?:…), or finditer and m.group(0).

05replace replaces once, sub replaces all

JavaScript's replace replaces the first match unless the regex has g. Python's re.sub replaces all of them unless you pass count:

"a-b-c".replace("-", "+");      // "a+b-c"
"a-b-c".replace(/-/g, "+");     // "a+b+c"
"a-b-c".replaceAll("-", "+");   // "a+b+c"
re.sub(r"-", "+", "a-b-c")           # "a+b+c"
re.sub(r"-", "+", "a-b-c", count=1)  # "a+b-c"

The replacement syntax differs too: $1 and $<name> in JavaScript, \1, \g<1> and \g<name> in Python.

06Lookbehind width

JavaScript allows any lookbehind, including variable-length ones:

Matches4/4 as expected
/(?<=\$)\d+(\.\d\d)?/

Should match2/2

  • $5
  • cost $12.50

Should not2/2

  • 5
  • USD 12
Try it in the Playground
A fixed-width lookbehind like (?<=\$) works in both languages.
Matches3/3 as expected
/(?<=USD ?)\d+/

Should match2/2

  • USD12
  • USD 12

Should not1/1

  • EUR 12
Try it in the Playground
The optional space makes this lookbehind variable-width. JavaScript accepts it.

Python's re rejects the second one with look-behind requires fixed-width pattern. Rewrite it as alternatives of fixed width, (?:(?<=USD)|(?<=USD ))\d+, or use the third-party regex module.

07Named groups are spelled differently

JavaScript Python
Define (?<year>\d{4}) (?P<year>\d{4})
Refer back \k<year> (?P=year)
In a replacement $<year> \g<year>
Read it m.groups.year m.group("year")

Python 3.12's re rejects the JavaScript form with unknown extension ?<n. A pattern with named groups won't move between the two without edits.

08Inline flags

Python accepts flags inside the pattern, at the start: (?i)abc. A global flag anywhere else is an error. Both languages support scoped flags on a group, (?i:a)b, which makes only a case-insensitive. In JavaScript that's new (ES2025, in Node 23 and current Chrome and Firefox), so check your targets. JavaScript has no leading (?i) at all; put flags after the slash.

09Things only one side has

  • Python only: atomic groups (?>…) and possessive quantifiers a++ (3.11+), \A and \Z, conditional groups (?(1)…).
  • JavaScript only: Unicode property escapes \p{L} with the u flag (Python needs the regex module), the sticky y flag, and match indices with d.

10Escaping in strings

Python patterns live in strings, so write them as raw strings, r"\d+", or every backslash needs doubling. JavaScript regex literals, /\d+/, need no extra escaping. But new RegExp("\\d+") builds from a string, and needs the doubled backslash just like a non-raw Python string.

11Try one

The live board runs JavaScript, so this lookbehind works here:

Live huntpar 4

Catch lines with a number right after a $ sign.

Targets · 0/4 caught

  • $5target
  • cost $12.50target
  • pay $100 nowtarget
  • $0target

Innocents · 0 hit

  • 5innocent
  • 5$innocent
  • USD 12innocent
  • $ 5innocent
  • $.50innocent

Type a regex — the board updates as you go.

Try it in the Playground
Like this? A new one drops every day.Play today’s hunt

12Checklist

  • JS test() searches. Python re.match only checks the start. Use re.search or re.fullmatch.
  • Python's $ allows a trailing newline. Use re.fullmatch or \Z.
  • Python's \d and \w are Unicode. Use [0-9] or re.ASCII.
  • Python's findall returns groups when the pattern has them.
  • JS replace changes the first match unless you use g or replaceAll. Python's sub changes all.
  • Python needs fixed-width lookbehind.
  • Named groups: (?<n>…) in JS, (?P<n>…) in Python.

13Keep practising