Why Named Capture Groups Exist
Numbered capture groups work fine when there's one or two of them, but patterns with five or six parentheses turn into a counting puzzle. match[3]says nothing about what's stored there — you have to re-read the regex to remember whether group 3 is month or day. Insert a new group somewhere in the middle and every downstream index shifts. Named capture groups fix this. You tag a sub-pattern with an identifier at write time and read it back by name at use time. The regex becomes self-documenting, the code becomes refactor-safe, and modern destructuring makes it ergonomic.
Syntax by Language
JavaScript (ES2018+) (?<name>pattern)
PCRE / PHP 7+ (?<name>pattern) — also (?'name'pattern)
Java 7+ (?<name>pattern)
.NET (?<name>pattern) — also (?'name'pattern)
Python re (?P<name>pattern) — the P prefix is required
Ruby (Onigmo) (?<name>pattern) — also (?'name'pattern)
Go regexp (?P<name>pattern) — Python-style
Rust regex (?P<name>pattern) — Python-styleThe behaviour is identical everywhere — only the syntax differs. Patterns written for JavaScript or PCRE need the P added for Python and Go, and vice versa.
Capturing Dates
// JavaScript
const date = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/;
const { groups } = "2026-10-02".match(date);
groups.year; // "2026"
groups.month; // "10"
groups.day; // "02"
// Direct destructuring
const { year, month, day } = "2026-10-02".match(date).groups;Capturing URL Parts
const url = /(?<proto>https?):\/\/(?<host>[^\/:?]+)(?::(?<port>\d+))?(?<path>\/[^?]*)?(?:\?(?<query>.+))?$/;
"https://api.promptspace.in:443/v1/items?page=2".match(url).groups;
// {
// proto: "https",
// host: "api.promptspace.in",
// port: "443",
// path: "/v1/items",
// query: "page=2"
// }Named Backreferences
A named backreference matches the same text that was already captured by a named group. This is especially useful for matched delimiters:
// Match quoted strings where opening and closing quote must match
const quoted = /(?<q>["'`]).*?\k<q>/g;
"\"hello\" and 'world' and `template`".match(quoted);
// => ['"hello"', "'world'", "`template`"]
// Mismatched quotes are rejected:
"\"hello'".match(quoted); // null — opening " does not match closing 'Named Groups in Replacement
// JavaScript — $<name>
"2026-10-02".replace(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/,
"$<day>/$<month>/$<year>");
// => "02/10/2026"
// Python — \\g<name>
import re
re.sub(r'(?P<y>\d{4})-(?P<m>\d{2})-(?P<d>\d{2})',
r'\g<d>/\g<m>/\g<y>', '2026-10-02')
# => "02/10/2026"
// PHP — ${name}
preg_replace('/(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/',
'${d}/${m}/${y}', '2026-10-02');Language-Specific Access Patterns
JavaScript
const re = /(?<key>\w+)=(?<value>[^;]+)/g;
const header = "user=alice; role=admin; theme=dark";
const parsed = {};
for (const m of header.matchAll(re)) {
parsed[m.groups.key] = m.groups.value;
}
// parsed => { user: "alice", role: "admin", theme: "dark" }Python
import re
LOG = re.compile(
r'^(?P<ip>\d+\.\d+\.\d+\.\d+) '
r'\[(?P<time>[^\]]+)\] '
r'"(?P<method>\w+) (?P<path>\S+)" '
r'(?P<status>\d{3})'
)
line = '192.168.1.1 [10/Oct/2026:14:33:00 +0000] "GET /api/items" 200'
m = LOG.match(line)
m.groupdict()
# {
# 'ip': '192.168.1.1',
# 'time': '10/Oct/2026:14:33:00 +0000',
# 'method': 'GET',
# 'path': '/api/items',
# 'status': '200'
# }PHP
$pattern = '/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/';
preg_match($pattern, '2026-10-02', $m);
// $m => [
// 0 => '2026-10-02',
// 'year' => '2026', 1 => '2026',
// 'month' => '10', 2 => '10',
// 'day' => '02', 3 => '02',
// ]
// Named keys and numbered keys both workCommon Pitfalls
Python needs the P prefix
Copying a JavaScript pattern (?<year>\d{4}) straight into Pythonre throws sre_constants.error: unknown extension ?<y. Python needs (?P<year>\d{4}). This is the number-one source of "my regex works in the tester but not in my code".
Name collisions
Most engines reject patterns that use the same name twice — (?<num>\d+)-(?<num>\d+) is a syntax error. .NET is the only major engine that allows duplicate names (the second overwrites the first on match). If you need OR branches with the same name across alternatives, PCRE and PHP support (?J) to allow duplicates; most other engines want distinct names.
Named group names must be valid identifiers
Letters, digits, underscores, starting with a letter. No hyphens, no spaces, no reserved words. If you need a name that starts with a digit for schema reasons, pad it: (?<_1>...).
Replacement syntax varies
JavaScript uses $<name>, Python uses \g<name>, PHP uses ${name}. Writing one and running it in another engine silently inserts the literal text as a replacement — a subtle bug that leaves the input unchanged.
Backreference syntax also varies
Most engines use \k<name>. Python re accepts both (?P=name) and \g<name> but NOT \k without a backslash. Always test the exact syntax your runtime expects before shipping.
Named Groups Cheatsheet
| Operation | JavaScript / PCRE | Python |
|---|---|---|
| Define | (?<name>...) | (?P<name>...) |
| Backreference | \k<name> | (?P=name) |
| In replacement | $<name> | \g<name> |
| Access result | m.groups.name | m.group("name") |
Testing Your Named Group Regex
Use the live Regex Tester above:
Test string:
"Published 2026-10-02, revised 2024-01-15, draft 2025-07-30."
Pattern:
/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/g
Expected: 3 matches, each exposing year, month, day as named captures.
Backreference pattern:
/(?<q>["'])\w+\k<q>/g
Test: "key='value' and name=\"john\" and bad='mix\""
Matches: "'value'", "\"john\"" — mismatched 'mix" rejected.Performance Notes
Named groups have the same runtime cost as numbered groups — the name is only a lookup table entry maintained during compilation. The engine still internally references groups by index. You can read a named match via either m.groups.name or m[1]in JavaScript, and they return the same string. Use names for clarity; the regex engine doesn't care.