Regex Syntax Reference: Where JavaScript, Python, PCRE and Go Disagree

Regular expression syntax looks standardised until you move a pattern between languages and it stops working. This page is a complete token reference plus — more usefully — a precise account of where the major engines disagree, so you know which parts of a pattern are portable and which are not.

The four flavours that matter

PCRE (PHP, and the reference implementation for most tooling), Python re, JavaScript, and RE2 (Go, and Rust's regex crate). The first three are backtracking engines with broadly similar features. RE2 is architecturally different: it guarantees linear time and therefore cannot support backreferences or lookaround at all.

Character classes and shorthands

TokenMatchesNotes
.Any character except newlineIncludes newline with the dotall flag
\dA digitUnicode-dependent — see below
\DNot a digit
\wWord characterASCII: [A-Za-z0-9_]. Unicode mode is wider
\WNot a word character
\sWhitespaceSpace, tab, newline, and more in Unicode mode
\SNot whitespace
[abc]Any one of a, b, c
[^abc]Any character except a, b, cAlso matches newline
[a-z]A range
\p{L}Any Unicode letterNeeds the u flag in JavaScript; unsupported in Python re
\bWord boundaryZero width — matches a position, not a character
\BNot a word boundary

⚠️ \d does not always mean 0–9

In Unicode mode, \d matches decimal digits from any script — Arabic-Indic ٤, Devanagari , Bengali and hundreds more. Python 3 does this by default for str patterns.

The consequence is a real validation bug: a "numeric" field validated with ^\d+$ accepts ٤٢, which then fails at int() or arrives in your database as something you did not expect. When you mean ASCII digits, write [0-9].

Quantifiers

GreedyLazyPossessiveMeaning
**?*+Zero or more
++?++One or more
????+Zero or one
{n}Exactly n
{n,}{n,}?{n,}+n or more
{n,m}{n,m}?{n,m}+Between n and m

Possessive quantifiers take as much as they can and refuse to give any back. They cannot change what a pattern matches, only how fast it fails — which makes them a targeted defence against backtracking. They exist in PCRE and Java, and not in JavaScript or Python.

// Greedy vs lazy against "<a><b><c>" /<.+>/ → "<a><b><c>" // takes everything, backs off to the last > /<.+?>/ → "<a>" // stops at the first > // Best of all — do not allow the delimiter inside the match /<[^>]+>/ → "<a>" // no backtracking needed at all

✅ Negated classes beat lazy quantifiers

[^>]+ is both faster and clearer than .+? for "everything up to the next delimiter". The lazy version tries a match, fails, extends by one character and retries. The negated class simply cannot cross the delimiter, so there is nothing to retry. On long strings the difference is substantial, and it also removes a common ReDoS shape.

Groups and references

SyntaxMeaningNotes
(...)Capturing groupNumbered from 1, left to right
(?:...)Non-capturing groupGroups without capturing — use this by default
(?<name>...)Named groupPCRE, JavaScript, Go. Python uses (?P<name>...)
\1, \2BackreferenceNot supported in RE2/Go
\k<name>Named backreferencePython uses (?P=name)
(?>...)Atomic groupPCRE, Java. Not in JavaScript or Python
|AlternationLowest precedence — group it if unsure

Named group syntax by language

// PCRE, JavaScript, Go, .NET (?<year>\d{4})-(?<month>\d{2}) # Python — note the P (?P<year>\d{4})-(?P<month>\d{2}) // Python also accepts the PCRE form since 3.12, but the P form // is what you will meet in existing code.

Lookaround

SyntaxNameMeaning
(?=...)Positive lookaheadNext text matches, but is not consumed
(?!...)Negative lookaheadNext text does not match
(?<=...)Positive lookbehindPreceding text matches
(?<!...)Negative lookbehindPreceding text does not match

Lookaround is zero width: it asserts something about a position without consuming characters. That is what makes it useful for conditions you do not want in the result.

// Price without capturing the currency symbol /(?<=\$)\d+\.\d{2}/ // "$19.99" → "19.99" // A word not followed by another /\bcat\b(?!\s+food)/ // matches "cat" but not "cat food" // Multiple conditions on one position — password rules /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{12,}$/ // Thousands separators, right to left "1234567".replace(/\B(?=(\d{3})+(?!\d))/g, ","); // "1,234,567"
EngineLookaheadLookbehindVariable-length lookbehind
PCRE / PHPYesYesNo — fixed width only
Python reYesYesNo — raises an error
Python regex moduleYesYesYes
JavaScriptYesYesYes
JavaYesYesBounded only
Go / RE2NoNoNo
.NETYesYesYes

JavaScript is unusually capable here — variable-length lookbehind is something Python's standard library still cannot do. A pattern like (?<=\w+:)\d+ works in JavaScript and raises look-behind requires fixed-width pattern in Python.

Anchors, and a security note

TokenMatches
^Start of string, or start of line with the multiline flag
$End of string, or end of line with the multiline flag
\AStart of string — always, regardless of flags
\zEnd of string — always
\ZEnd of string, allowing one trailing newline

🚨 In Python, $ matches before a trailing newline

re.match(r'^\w+$', 'admin\n') succeeds. Python's $ matches at the end of the string or just before a final newline, so a value with a trailing newline passes validation designed to reject it. Where that value is then used in a shell command, a header, or a log line, the newline can be exploited.

Use \A...\z for strict whole-string validation in Python. JavaScript's $ does not have this behaviour without the multiline flag.

Flags by language

BehaviourJavaScriptPythonPCRE
Case-insensitiveire.Ii
Global / find allgfindall()g
Multiline ^$mre.Mm
Dot matches newlinesre.Ss
Unicode modeuDefaultu
Verbose / commentedNonere.Xx
Stickyy

The missing verbose flag is a genuine gap in JavaScript. Python and PCRE let you write a complex pattern across multiple lines with comments, which transforms maintainability:

import re pattern = re.compile(r""" ^ (?P<protocol>https?) # http or https :// (?P<host>[^/:\s]+) # hostname (?::(?P<port>\d+))? # optional port (?P<path>/[^\s?#]*)? # optional path $ """, re.VERBOSE)

Catastrophic backtracking

This is the failure mode worth understanding properly, because it turns a regex into a denial-of-service vector.

Backtracking engines try alternatives until one matches. Some pattern shapes give the engine an exponential number of alternatives to try before it can conclude that nothing matches. The pattern works fine on matching input and hangs on input that almost matches.

// The classic trigger — nested quantifiers /^(a+)+$/ // Against "aaaaaaaaaaaaaaaaaaaaaaaaX": // the engine must try every way of splitting 24 a's // between the inner + and the outer + // = 2^24 attempts before it can report failure // Same shape, hidden in something plausible /^(\w+\s?)*$/ // ← dangerous /^([a-zA-Z]+)*$/ // ← dangerous /(\d+)+$/ // ← dangerous

The warning sign is a quantifier applied to a group that itself contains a quantifier, where the inner and outer can match the same characters. The engine has no way to know which split is correct, so it tries them all.

FixExampleAvailable in
Remove the nesting^(a+)+$^a+$Everywhere
Use a negated class".*?""[^"]*"Everywhere
Bound the repetition\w+\w{1,64}Everywhere
Atomic group(?>\w+)PCRE, Java
Possessive quantifier\w++PCRE, Java
Use a linear-time engineGo's regexp, Rust's regexRE2-based
Cap the input lengthReject before matchingEverywhere

💡 Why Go refuses your pattern

RE2 — used by Go and Rust — guarantees linear-time matching by construction. It achieves that by omitting backreferences and lookaround entirely, because those are precisely the features that make exponential behaviour expressible. So a pattern using \1 or (?=...) does not merely fail in Go; it cannot be supported without giving up the guarantee. For user-supplied input this trade is usually worth taking.

Common patterns

GoalPattern
Whole word\bword\b
Trim whitespace^\s+|\s+$
Collapse whitespace\s+" "
Quoted string"[^"\\]*(?:\\.[^"\\]*)*"
HTML tag<[^>]+>
Hex colour#(?:[0-9a-fA-F]{3}){1,2}\b
ISO date\d{4}-\d{2}-\d{2}
IPv4 (loose)\b(?:\d{1,3}\.){3}\d{1,3}\b
Duplicate word\b(\w+)\s+\1\b
Leading zeros^0+(?=\d)
camelCase split(?<=[a-z])(?=[A-Z])

⚠️ Do not validate email addresses with regex

The grammar in RFC 5322 permits quoted local parts, comments, and nested structures that no practical pattern captures. The fully correct regex is thousands of characters long, and even then it cannot tell you whether the mailbox exists — which is the thing you actually care about.

Check for an @ with something on either side, then send a confirmation message. That is the only validation that means anything.

A portability checklist

Before moving a pattern between languages, check these six things:

  1. Named groups — Python needs (?P<name>), everyone else uses (?<name>).
  2. Lookbehind width — Python's built-in re requires fixed length; JavaScript does not.
  3. $ semantics — Python allows a trailing newline. Use \z for strictness.
  4. Unicode defaults — Python 3 is Unicode by default; JavaScript needs the u flag for \p{...}.
  5. Verbose mode — does not exist in JavaScript. Multi-line patterns must be concatenated.
  6. Backreferences and lookaround — will not compile at all in Go or Rust.

Test a pattern before you ship it

Try your regex against real sample text, see every match and capture group highlighted, and catch the edge case before production does.

Open the Regex Tester →

Summary

  • Prefer negated classes to lazy quantifiers — faster, clearer, and safer.
  • \d is not [0-9] in Unicode mode. Be explicit when validating.
  • Python's $ allows a trailing newline. Use \A...\z for real validation.
  • Nested quantifiers cause catastrophic backtracking. Never run one against user input.
  • Go and Rust have no backreferences or lookaround — that is a deliberate safety guarantee.
  • Use non-capturing groups (?:...) unless you need the capture.
  • Comment complex patterns with verbose mode where the language allows it.

Frequently Asked Questions

Does JavaScript support lookbehind in regex?

Yes. Lookbehind — (?<=...) and (?

Why does \\d match more than 0-9 in some languages?

Because in Unicode mode some engines make \\d match any decimal digit in any script, including Arabic-Indic and Devanagari numerals. Python 3 does this by default for str patterns. If you mean ASCII digits specifically, write [0-9] or use an ASCII flag — this matters for validation, where a non-ASCII digit will pass \\d and then fail integer parsing.

What is catastrophic backtracking?

A pattern shape where the engine must try an exponential number of ways to match before concluding it cannot. Nested quantifiers like (a+)+ against a long non-matching string are the classic trigger. On a server this turns one crafted input into a hung CPU core — a denial of service known as ReDoS.

Which regex flavour does Go use?

Go's regexp package uses RE2, which guarantees linear-time matching and therefore cannot suffer catastrophic backtracking. The trade-off is that RE2 deliberately omits backreferences and lookaround, because those features are what make exponential behaviour possible. If a pattern with \\1 or (?=...) fails to compile in Go, this is why.

What is the difference between greedy and lazy quantifiers?

A greedy quantifier such as .* takes as much as possible then gives characters back until the rest of the pattern matches. A lazy quantifier such as .*? takes as little as possible then adds characters. Matching <.+> against '' captures the whole string; <.+?> captures just ''.

P

Written by Paras

We build free, browser-based file tools and write the reference material we wish existed when we were looking things up. Spotted an error? Tell us and we will fix it.