Regex Flavors Compared Across 9 Languages

Regex is not one language, and the differences between engines are worse documented than almost anything else developers use daily. Half the answers you will find are stale — Python gained atomic groups in 3.11, Go accepted a second named-group syntax in 1.22 without mentioning it in the release notes, and PHP allows variable-length lookbehind since 8.4 while php.net's own manual still says it does not. This guide is a feature matrix verified against primary documentation on 2026-07-24, with a section on the claims that are now simply wrong. Check a specific pattern with the regex flavor checker.

The short version

  • Go and Rust have no look-around and no backreferences. This is deliberate — it buys a linear-time guarantee no other engine here offers.
  • Lookbehind width is the most confused topic in regex. Every engine has a different rule and most summaries you will read are wrong. See the table below.
  • Python gained atomic groups and possessive quantifiers in 3.11. Writing that says otherwise predates it.
  • JavaScript has no verbose/x flag, despite ES2025 shipping inline modifiers for i, m and s.
  • The dangerous differences are the silent ones\p{L} without u in JavaScript, and Ruby's /m.

Feature matrix

Verified 2026-07-24 against primary documentation. Baseline versions: Python 3.14, ECMAScript 2026, JDK 25 LTS, .NET 10, Go 1.26, PHP 8.5 (PCRE2 10.44), Ruby 4.0, Rust regex 1.13.1, Kotlin/JVM.

FeaturePythonJSJava.NETGoPHPRubyRust
LookaheadYesYesYesYesNoYesYesNo
LookbehindFixed onlyVariableSee belowVariableNoVariable (8.4+)Fixed per alt.No
BackreferencesYesYesYesYesNoYesYes*No
Atomic groups3.11+NoYesYesNoYesYesNo
Possessive quantifiers3.11+NoYesNoNoYesPartial*No
RecursionNoNoNoNoNoYes\g<> onlyNo
ConditionalsYesNoNoYesNoYes2.0+No
\p{…}NoNeeds uYesNo scriptsYesYesYesYes
Verbose x modeYesNoPartialYesNoYesYesYes
Linear-time guaranteeNoNoNoOpt-in*YesNoPartial*Yes

Kotlin/JVM delegates entirely to java.util.regex, so the Java column applies verbatim. Kotlin/JS is a different engine — see the caveats. Asterisked cells are explained below.

Lookbehind: every engine has a different rule

This deserves its own section because it is where portability breaks most often, and because the two competing summaries you will find online — "lookbehind must be fixed-width" and "lookbehind must be bounded" — are each wrong for most engines.

EngineRule
PythonStrictly fixed width. Even bounded (?<=x{2,3}) is rejected. a|b is fine; a* and a{3,4} are not.
JavaScriptUnrestricted. The spec production is a plain disjunction. Also matches right-to-left, which changes sub-capture contents.
.NETUnrestricted. The docs say so explicitly, because the engine does full right-to-left matching.
PHPVariable from 8.4 (PCRE2 10.43+), max 255 characters per branch; unlimited repetition still rejected. Fixed-only on 8.3 and earlier.
RubyFixed width per top-level alternative. (?<=a|bc) is accepted; (?<=a+) is not.
JavaUndocumented. See below.
Go, RustNo lookbehind at all.

Java is the odd one. The Pattern javadoc contains no statement about lookbehind width in any current JDK — the rule is simply unspecified. Observed behaviour on OpenJDK 21 is that unbounded quantifiers on a single-character atom compile and match correctly ((?<=a+)b, (?<=\w+)c all work), while unbounded quantifiers on a multi-character group raise Look-behind group does not have an obvious maximum length. Bounded groups such as (?:ab){1,3} are accepted. Treat this as implementation behaviour rather than a contract.

One more PHP wrinkle worth knowing: PHP can be built against an external system PCRE2 as old as 10.30, so a distribution PHP 8.4 may still reject variable-length lookbehind. Check the PCRE_VERSION constant rather than the PHP version.

Claims that are now wrong

Each of these is repeated widely and is currently false. They are the fastest way to waste an afternoon.

  • "Python has no atomic groups or possessive quantifiers." Both landed in Python 3.11. True only for 3.10 and earlier.
  • "Python has no conditionals." (?(1)yes|no) has worked for years, named groups included. What stdlib lacks is a lookaround as the test.
  • "You can write (?<name>…) in Python." Not in stdlib re — it is (?P<name>…) only, and the angle form raises a PatternError.
  • "JavaScript has no lookbehind." Shipped in ES2018; widely available across browsers since March 2023. Safari's late support is why the myth persists.
  • "ES2025 gave JavaScript an x flag." The modifiers proposal shipped i, m and s only. (?x:a) throws.
  • "\p{L} works in JavaScript without a flag." It does not throw — it silently matches the literal text p{L}.
  • "Java needs (?U) for \p{…}." No. UNICODE_CHARACTER_CLASS only upgrades \w \d \s \b and POSIX classes.
  • ".NET Singleline means multi-line." It means dotall. Multiline is the per-line anchor one.
  • ".NET supports \p{IsLatin}." .NET has no script properties at all — only general categories and Unicode-4.0-era named blocks.
  • ".NET has possessive quantifiers." It never has, including .NET 10. Use an atomic group.
  • "Go only accepts (?P<name>…)." (?<name>…) works from Go 1.22 — undocumented in the release notes, which is why this one is nearly universally stale.
  • "Go has a verbose flag." Go's flags are exactly i, m, s, U.
  • "PCRE lookbehind must be fixed-length." Wrong for PHP 8.4+ — and php.net's own Assertions page is stale on this point.
  • "Ruby's /m is multi-line." It is dotall. Ruby's ^/$ are always line anchors.
  • "Ruby has no conditionals." Added in Ruby 2.0 with the Onigmo merge.
  • "Ruby is unguarded against ReDoS." Ruby 3.2 added memoization and Regexp.timeout=. But see the caveat below.

Asterisked caveats

Ruby backreferences. They work — until you introduce a named group. From that point every unnamed (…) becomes non-capturing and \1 raises a SyntaxError. Name every group you intend to capture.

Ruby possessive quantifiers. ?+, *+ and ++ are possessive, but {n,m}+ is not — it parses as a repeated group. Compare /\Aa{1,2}+a\z/ =~ "aa" (matches at 0) with /\Aa*+a\z/ =~ "aa" (nil).

Ruby's linear-time memoization. Ruby 3.2 made roughly 90% of patterns linear, and 3.3 extended it to lookarounds and atomic groups — but it is skipped for backreferences, subexpression calls, nested lookarounds and nested counted quantifiers, and it consumes memory proportional to input length. Ruby's own documentation warns that Regexp.linear_time? returning true still does not make untrusted input safe. Regexp.timeout defaults to nil.

.NET NonBacktracking. Available from .NET 7, but it bans lookarounds, backreferences, atomic groups, balancing groups, conditionals and \G — most of what people want to protect. It is also incompatible with RightToLeft and ECMAScript, and unsupported by the [GeneratedRegex] source generator.

Kotlin. On the JVM, Regex delegates to java.util.regex and exposes toPattern(), so every Java row applies exactly. Kotlin/JS is a different story: it compiles to JavaScript RegExp and exposes only 2 of the 7 RegexOption values, so code using COMMENTS, DOT_MATCHES_ALL, LITERAL, UNIX_LINES or CANON_EQ will not compile for JS or common targets.

Writing patterns that port

If a pattern has to work everywhere, the safe subset is narrower than most people assume: character classes, quantifiers, alternation, anchors, non-capturing groups, and lookahead. That is roughly it. Drop lookbehind, backreferences, atomic groups, possessive quantifiers, recursion and conditionals, and you will run on all nine engines.

If you only need the mainstream backtracking engines and can exclude Go and Rust, you additionally get lookbehind and backreferences — but you still need to watch width rules, name your groups consistently, and remember that flags do not mean the same thing across languages. And once Go or Rust is in scope, the question stops being "which syntax" and becomes "can this be expressed without look-around at all," which is usually a matter of restructuring the match and doing the rest in code.

The flavor checker answers that per-pattern; the regex tester and explainer help once you have settled on one.

Frequently Asked Questions

Which regex engines guarantee linear-time matching?

Two: Go (RE2) and Rust (the regex crate). Go's documentation states it is "guaranteed to run in time linear in the size of the input," and Rust specifies worst-case O(m·n) where m is the regex size and n the haystack size. Neither can suffer catastrophic backtracking. The cost is that both must omit look-around and backreferences, which cannot be implemented within that guarantee. Everything else on this list is a backtracking engine and can be made to hang by a hostile pattern. Partial mitigations exist: .NET has RegexOptions.NonBacktracking (from .NET 7), and Ruby added memoization in 3.2 — but see the caveats, because neither is the drop-in fix it appears to be.

Is RegexOptions.NonBacktracking a drop-in ReDoS fix for .NET?

No, and this catches people. NonBacktracking bans exactly the constructs that patterns needing protection tend to use: lookarounds, backreferences, atomic groups, balancing groups, conditionals and \G. It is also mutually exclusive with RightToLeft and ECMAScript, and the [GeneratedRegex] source generator does not support it. Separately, Regex.MatchTimeout defaults to InfiniteMatchTimeout, so you are not protected by a timeout unless you set one explicitly.

What is the difference between Ruby's /m and Python's re.M?

They mean opposite things, which makes this one of the nastiest silent porting bugs. Ruby's /m is dotall — it makes . match a newline, equivalent to Python's re.S / (?s). Ruby has no multi-line flag at all, because ^ and $ are always line anchors in Ruby; if you want string anchors you must use \A and \z. Python's re.M does the opposite job: it turns ^ and $ into line anchors, which they are not by default. The constant name Regexp::MULTILINE in Ruby refers to the dotall behaviour, which does not help.

Does Go support (?<name>...) for named groups?

Yes, since Go 1.22 — both (?P<name>…) and (?<name>…) are accepted. Almost every reference still says (?P<name>…) is the only form, because the change was not mentioned in the Go 1.22 release notes; it is visible only in the regexp/syntax documentation and the commit. Note that (?'name'…) is still rejected, and that Go has no way to reference a named group from within the pattern, because RE2 has no backreferences at all.

Why does my numbered backreference break when I add a named group in Ruby?

Because Ruby changes the meaning of unnamed groups once any named group is present. Introduce a single (?<name>…) and every plain (…) in that pattern becomes non-capturing, so \1 raises SyntaxError: numbered backref/call is not allowed. (use name). The fix is to name every group you intend to capture. No other engine on this list behaves this way.