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
| Token | Matches | Notes |
|---|---|---|
. | Any character except newline | Includes newline with the dotall flag |
\d | A digit | Unicode-dependent — see below |
\D | Not a digit | |
\w | Word character | ASCII: [A-Za-z0-9_]. Unicode mode is wider |
\W | Not a word character | |
\s | Whitespace | Space, tab, newline, and more in Unicode mode |
\S | Not whitespace | |
[abc] | Any one of a, b, c | |
[^abc] | Any character except a, b, c | Also matches newline |
[a-z] | A range | |
\p{L} | Any Unicode letter | Needs the u flag in JavaScript; unsupported in Python re |
\b | Word boundary | Zero width — matches a position, not a character |
\B | Not 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
| Greedy | Lazy | Possessive | Meaning |
|---|---|---|---|
* | *? | *+ | 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.
✅ 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
| Syntax | Meaning | Notes |
|---|---|---|
(...) | Capturing group | Numbered from 1, left to right |
(?:...) | Non-capturing group | Groups without capturing — use this by default |
(?<name>...) | Named group | PCRE, JavaScript, Go. Python uses (?P<name>...) |
\1, \2 | Backreference | Not supported in RE2/Go |
\k<name> | Named backreference | Python uses (?P=name) |
(?>...) | Atomic group | PCRE, Java. Not in JavaScript or Python |
| | Alternation | Lowest precedence — group it if unsure |
Named group syntax by language
Lookaround
| Syntax | Name | Meaning |
|---|---|---|
(?=...) | Positive lookahead | Next text matches, but is not consumed |
(?!...) | Negative lookahead | Next text does not match |
(?<=...) | Positive lookbehind | Preceding text matches |
(?<!...) | Negative lookbehind | Preceding 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.
| Engine | Lookahead | Lookbehind | Variable-length lookbehind |
|---|---|---|---|
| PCRE / PHP | Yes | Yes | No — fixed width only |
Python re | Yes | Yes | No — raises an error |
Python regex module | Yes | Yes | Yes |
| JavaScript | Yes | Yes | Yes |
| Java | Yes | Yes | Bounded only |
| Go / RE2 | No | No | No |
| .NET | Yes | Yes | Yes |
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
| Token | Matches |
|---|---|
^ | Start of string, or start of line with the multiline flag |
$ | End of string, or end of line with the multiline flag |
\A | Start of string — always, regardless of flags |
\z | End of string — always |
\Z | End 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
| Behaviour | JavaScript | Python | PCRE |
|---|---|---|---|
| Case-insensitive | i | re.I | i |
| Global / find all | g | findall() | g |
Multiline ^$ | m | re.M | m |
| Dot matches newline | s | re.S | s |
| Unicode mode | u | Default | u |
| Verbose / commented | None | re.X | x |
| Sticky | y | — | — |
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:
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 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.
| Fix | Example | Available 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 engine | Go's regexp, Rust's regex | RE2-based |
| Cap the input length | Reject before matching | Everywhere |
💡 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
| Goal | Pattern |
|---|---|
| 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:
- Named groups — Python needs
(?P<name>), everyone else uses(?<name>). - Lookbehind width — Python's built-in
rerequires fixed length; JavaScript does not. $semantics — Python allows a trailing newline. Use\zfor strictness.- Unicode defaults — Python 3 is Unicode by default; JavaScript needs the
uflag for\p{...}. - Verbose mode — does not exist in JavaScript. Multi-line patterns must be concatenated.
- 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.
\dis not[0-9]in Unicode mode. Be explicit when validating.- Python's
$allows a trailing newline. Use\A...\zfor 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 ''.