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.

· 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:
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 quantifiersa++(3.11+),\Aand\Z, conditional groups(?(1)…). - JavaScript only: Unicode property escapes
\p{L}with theuflag (Python needs theregexmodule), the stickyyflag, and match indices withd.
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:
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.
12Checklist
- JS
test()searches. Pythonre.matchonly checks the start. Usere.searchorre.fullmatch. - Python's
$allows a trailing newline. Usere.fullmatchor\Z. - Python's
\dand\ware Unicode. Use[0-9]orre.ASCII. - Python's
findallreturns groups when the pattern has them. - JS
replacechanges the first match unless you usegorreplaceAll. Python'ssubchanges all. - Python needs fixed-width lookbehind.
- Named groups:
(?<n>…)in JS,(?P<n>…)in Python.
13Keep practising
- The guide's flavors section compares regex engines side by side.
- The posts on lookahead and lookbehind and named groups go deeper on two of these.
- Try today's hunt, or browse past boards in the archive.