BytePane

Regex Named Groups in JavaScript vs Python: (?<name>) vs (?P<name>)

Regex6 min read

Quick answer

JavaScript defines a named group as (?<name>...), reads it via match.groups.name, backreferences it with \k<name>, and substitutes it with $<name>. Python defines it as (?P<name>...), reads it via m.group('name') or m.groupdict(), backreferences it with (?P=name), and substitutes it with \g<name>. The P is mandatory in Python: (?<name>) raises re.error: unknown extension.

JavaScript: define, read, replace

Per MDN, the groups object is available on the results of exec(), match(), and matchAll(), and is passed as the last argument to a replace() callback. Everything below was run on Node 20:

const re = /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/
const m = '2026-09-17'.match(re)

console.log(m.groups)             // [Object: null prototype] { year: '2026', month: '09', day: '17' }
console.log(m.groups.year, m[1])  // 2026 2026   (named groups keep their number)

const { year, month, day } = m.groups
console.log(`${day}/${month}/${year}`)               // 17/09/2026

// $<name> in a replacement string
console.log('2026-09-17'.replace(re, '$<day>/$<month>/$<year>'))   // 17/09/2026

// groups object is the last callback argument
console.log('2026-09-17'.replace(re, (...args) => {
  const g = args.at(-1)
  return `${g.month}-${g.day}`
}))                                                     // 09-17

// \k<name> backreference: match the same quote that opened the string
console.log(/(?<q>['"]).*?\k<q>/.exec(`say "hi" now`)[0])   // "hi"

// matchAll with the g flag
const log = 'GET /a 200\nPOST /b 404'
for (const x of log.matchAll(/(?<method>GET|POST) (?<path>\S+) (?<status>\d{3})/g)) {
  console.log(x.groups.method, x.groups.path, x.groups.status)
}
// GET /a 200
// POST /b 404

Note that groups has a null prototype, so m.groups.hasOwnProperty does not exist; use 'year' in m.groups or Object.hasOwn.

Python: define, read, replace

The re docs state that a symbolic group is also a numbered group and that each name must be defined only once. Same examples, same inputs, Python 3.10:

import re

pat = re.compile(r'(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})')
m = pat.match('2026-09-17')

print(m.groupdict())                        # {'year': '2026', 'month': '09', 'day': '17'}
print(m.group('year'), m.group(1), m['year'])   # 2026 2026 2026

# \g<name> in a replacement string
print(pat.sub(r'\g<day>/\g<month>/\g<year>', '2026-09-17'))     # 17/09/2026

# callback receives the Match object
print(pat.sub(lambda m: f"{m['month']}-{m['day']}", '2026-09-17'))   # 09-17

# (?P=name) backreference
print(re.search(r"(?P<q>['\"]).*?(?P=q)", 'say "hi" now').group(0))   # "hi"

# finditer for every match
for x in re.finditer(r'(?P<method>GET|POST) (?P<path>\S+) (?P<status>\d{3})', 'GET /a 200\nPOST /b 404'):
    print(x.group('method'), x['path'], x['status'])
# GET /a 200
# POST /b 404

\g<name> exists for a reason: \\20 in a replacement means group 20, while \g<2>0 means group 2 followed by a literal 0.

Syntax table

OperationJavaScriptPython re
Define a named group(?<year>\d{4})(?P<year>\d{4})
Backreference inside the pattern\k<year>(?P=year)
Read a group from the matchm.groups.yearm.group('year') or m['year']
All named groups as an object/dictm.groupsm.groupdict()
Reference in a replacement string$<year>\g<year>
Reference in a replacement callbacklast argument is the groups objectm.group('year') on the Match
Unmatched optional groupundefinedNone
Same name twiceSyntaxError (ES2025: allowed across alternatives)re.error: redefinition of group name
Also numbered?Yes, m[1]Yes, m.group(1)

The errors you get when you mix them up

# Python: JavaScript syntax
>>> re.compile(r'(?<x>a)')
re.error: unknown extension ?<x at position 1

# Python: same name twice
>>> re.compile(r'(?P<n>a)(?P<n>b)')
re.error: redefinition of group name 'n' as group 2; was group 1 at position 12

// JavaScript: same name twice in one alternative (Node 20)
new RegExp('(?<n>a)(?<n>b)')
// SyntaxError: Invalid regular expression: /(?<n>a)(?<n>b)/: Duplicate capture group name

// JavaScript: Python syntax is simply not a named group
// /(?P<x>a)/ -> SyntaxError: Invalid group

Unmatched groups behave consistently in each language: 'a'.match(/(?<x>a)(?<y>b)?/).groups gives { x: 'a', y: undefined }, and re.match(r'(?P<x>a)(?P<y>b)?', 'a').groupdict() gives {'x': 'a', 'y': None}. The key is always present.

Primary sources: MDN: Named capturing group, Python re module.

Frequently Asked Questions

Why does Python raise "unknown extension ?<x" for (?<name>...)?

Because Python's re module only documents the (?P<name>...) form; the P is required. The JavaScript-style (?<name>...) is not a valid Python named group, so re.compile raises re.error: unknown extension ?<x at position 1. In Python, (?<...) is reserved for lookbehind assertions such as (?<=...) and (?<!...).

Are named groups also numbered?

Yes, in both languages. A named group takes the next group number exactly as an unnamed group would, so m[1] in JavaScript and m.group(1) in Python return the same text as m.groups.year and m.group("year"). You can mix numbered and named access, but named access is far more robust when the pattern changes.

What do I get for a named group that did not match?

JavaScript: the key exists on match.groups with the value undefined. Python: m.groupdict() contains the key with the value None (or the default you pass to groupdict(default=...)). In both cases the key is present, so destructuring and dictionary access do not throw.

Can two groups share the same name?

Python: never; re.compile raises "redefinition of group name". JavaScript: not within the same alternative (SyntaxError: Duplicate capture group name). ES2025 permits the same name in different alternatives of a disjunction, but Node 20 still rejects it, so check your engine before relying on it.

Try it in the browser

  • Regex Tester, paste the JavaScript patterns above and inspect the groups output live.
  • Regex Pattern Library, 100+ ready-made patterns for dates, IDs, tokens, and more.

Related guides