DEV Community

EvvyTools
EvvyTools

Posted on

Why the Same Regex Can Match Differently in JavaScript vs Python

A regex pattern that works perfectly in a Python script can behave differently when you paste it straight into JavaScript, and the reverse happens just as often. Regex looks like a universal language because the basic syntax, character classes, quantifiers, groups, is shared across almost every engine. The differences show up in the details, and those details are exactly the ones that bite people porting a pattern from one language to another.

Multiline and Dot-Matches-Newline Behavior Differ

In Python, re.DOTALL makes . match newlines, and it's off by default. In JavaScript, the equivalent is the s flag, also off by default, but JavaScript added it years after Python and some older code relies on workarounds like [\s\S] instead. If you're porting a pattern that assumes . matches everything including newlines, check which flag your target language actually needs, because the flag names and defaults aren't interchangeable.

Named Group Syntax Is Shared, but Backreference Syntax Isn't Always

Both languages support (?<name>...) for named capture groups, which is genuinely convenient. Where they diverge is in referencing that group later in the same pattern: Python uses (?P=name), while JavaScript uses \k<name>. Copy a Python backreference into a JavaScript pattern unchanged and it won't just fail quietly, in most cases it throws a syntax error immediately, which is at least easier to catch than a silent behavior difference.

Unicode Handling Is a Common Source of Silent Bugs

Python 3's re module treats strings as Unicode by default. JavaScript needs the u flag (or v in newer engines) to get full Unicode-aware matching, particularly for character classes and quantifiers applied to characters outside the basic multilingual plane, like many emoji. A pattern that correctly counts characters in a string with accented letters or emoji in Python can silently miscount in JavaScript if the u flag is missing, because without it JavaScript is matching UTF-16 code units, not full characters.

Lookbehind Support Varies by Engine Version

Python's re module has supported fixed-width lookbehind for a long time. JavaScript only added lookbehind assertions in relatively recent engine versions, and while it's now broadly supported in modern browsers and Node, if you're targeting an older runtime or a stripped-down environment, a pattern using (?<=...) might simply not be supported at all rather than behaving differently.

Flags Look Similar but Aren't a One-to-One Map

Both languages use single-letter flags, which makes it tempting to assume they map directly onto each other. Mostly they do: i for case-insensitive matching and g for global matching (or Python's re.findall/finditer pattern, since Python doesn't have a literal g flag the way JavaScript does) behave similarly enough. Where it gets less intuitive is JavaScript's sticky flag, y, which anchors matching to lastIndex and has no direct Python equivalent, Python code doing the same thing typically uses re.match with an explicit position argument instead. If you're translating a JavaScript tokenizer loop that relies on the sticky flag into Python, there isn't a flag to flip, you need to restructure the matching loop itself.

A Concrete Side-by-Side Example

Take a pattern meant to extract a version number like v2.14.0 from a string, using a named group for the numeric part: in JavaScript, /v(?<version>\d+\.\d+\.\d+)/.exec(str).groups.version, and in Python, re.search(r'v(?P<version>\d+\.\d+\.\d+)', s).group('version'). The pattern body is identical. The named-group syntax is identical. The only differences are the method calls and how the result object is accessed, .groups.version versus .group('version'). This is a useful pattern to keep in mind generally: the regex syntax itself often ports cleanly, and the friction is almost always in the surrounding language-specific API, not the pattern.

Lookaheads Are More Consistently Supported Than Lookbehind

Both positive and negative lookahead, (?=...) and (?!...), have been supported in both languages for a long time and behave consistently between them. Lookbehind is the newer, less consistently supported feature, which is why it's worth double-checking your target JavaScript runtime's version if a pattern relies on it, particularly if the code needs to run in an older browser or a constrained embedded environment rather than a modern Node.js service.

Character Class Shorthand Isn't Always Identical Either

\d, \w, and \s look like they'd behave identically everywhere, and mostly they do for ASCII input, but Unicode awareness changes what they actually match. In Python 3 with default Unicode strings, \w matches Unicode word characters broadly, including letters from non-Latin scripts. In JavaScript without the u flag, \w is restricted to ASCII letters, digits, and underscore only. This means a pattern validating "word characters" against a name containing accented letters or non-Latin script can pass in Python and silently fail to match the full string in JavaScript, purely because of this default difference. It's a subtle one because both patterns compile without error and both "work" on ASCII test data, which is exactly the kind of test data most people reach for first.

Case Sensitivity of Flags Themselves

This is a smaller thing, but worth knowing: JavaScript flags are case-sensitive single letters appended after the closing slash, /pattern/gi, and there's no space or separator between them. Python's flags are passed as a separate argument, often combined with a bitwise OR, re.IGNORECASE | re.MULTILINE, or as inline flags at the start of the pattern string, (?im)pattern. Porting code between the two means translating not just which flags are set, but the entirely different mechanism for setting them, which is an easy thing to get wrong under time pressure if you're used to one style and quickly translating to the other.

Global Matching Changes State Differently Too

JavaScript's global flag g has a quirk that catches people off guard: when you use a global regex with .exec() in a loop, the regex object itself keeps mutable state via lastIndex, tracking where the next search should start. Forget to reset it between separate uses of the same compiled pattern, and you can get matches starting from the wrong position, or silently no matches at all if lastIndex is left past the end of a shorter string. Python's approach avoids this entirely: re.finditer returns a fresh iterator each call with no mutable state attached to the compiled pattern object itself. A JavaScript regex literal used inline in a loop, /pattern/g, actually gets a new instance each time it's evaluated in most cases, but a regex stored in a variable and reused across iterations is exactly where this state-tracking bug tends to appear.

Escaping Special Characters Isn't Identical Either

Building a regex dynamically from a variable (say, searching for user-provided text literally rather than as a pattern) requires escaping regex special characters first, and the recommended way to do this differs by language. JavaScript has no built-in escape function in most environments short of a small utility function or a recent RegExp.escape proposal still making its way through standardization, while Python provides re.escape() directly in the standard library. Porting code that builds a dynamic pattern from user input is a good moment to double check whether the target language's escaping approach is actually equivalent, since an incomplete escape function is a real source of both broken matches and, in worse cases, unintended pattern injection from unescaped special characters in the input.

The Practical Takeaway

None of these differences mean regex is unreliable across languages, they mean it's not automatically portable the way basic arithmetic is. If you're taking a pattern from a Stack Overflow answer, a library's source code, or another part of your own codebase written in a different language, treat it as a starting point to re-test, not a drop-in that's guaranteed to behave identically.

Running the ported pattern through a free regex tester from EvvyTools against the same test inputs you used in the original language is the fastest way to catch a flag mismatch or a syntax difference before it ships, especially for Unicode edge cases that won't show up unless your test string actually contains non-ASCII characters.

MDN's regular expressions guide, Python's own re module documentation, and Wikipedia's overview of regular expressions for the underlying theory are all worth keeping open side by side if you're doing this kind of porting regularly, since flag names and default behaviors are exactly the kind of detail that's easy to misremember under time pressure. EvvyTools also has a deeper explainer on capture group naming conventions that applies across both languages once you're past the basic syntax differences.

Top comments (0)