本页面有中文版。
Cette page est disponible en français.
Esta página está disponible en español.
Guides1 live hunt inside

Named groups and non-capturing groups in regex

Parentheses do two jobs: they group and they capture. How non-capturing groups (?:…) group without capturing, how named groups (?<year>…) make captures readable, how to refer back to them with \k<name>, and how Python spells it.

Regex Hunter logo

· 4 min read

/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/

match

  • 2026-10-13
  • 1999-12-31
  • 2000-01-01
  • 2026-02-28

spare

  • 26-10-13
  • 2026/10/13
  • 2026-1-3
  • 13-10-2026

Parentheses in a regex do two jobs at once. They group, so a quantifier or a | applies to everything inside. And they capture, saving the matched text so you can use it later.

Often you only want one of the two. This post covers the two kinds of group that split the jobs apart: non-capturing groups and named groups. Every example is live, so edit the patterns and watch the lines change.

01Plain groups capture

A plain group saves what it matched, numbered from 1 by the position of its opening parenthesis:

const m = "2026-10-13".match(/(\d{4})-(\d{2})-(\d{2})/);

m[0];  // "2026-10-13", the whole match
m[1];  // "2026"
m[2];  // "10"
m[3];  // "13"

That works until the pattern changes. Add a group near the start and every number after it shifts, and m[2] quietly becomes something else.

02Non-capturing groups: (?:…)

Write (?: instead of ( when you need grouping but don't care about the text. Here the group only exists so + repeats "word, then a space":

Matches4/4 as expected
/^(?:\w+ )+\w+$/

Should match2/2

  • two words
  • one two three

Should not2/2

  • single
  • double space
Try it in the Playground
(?: … ) groups "a word and a space" so + can repeat it. Nothing is captured.

It matches exactly like (\w+ )+\w+. The difference is only in what you get back: no capture to number, and nothing to shift when you edit the pattern. It's also a little cheaper, since the engine doesn't save the text.

A good habit: use (?:…) for every group you don't read later. Then the numbered or named groups that remain are the ones that mean something.

03Named groups: (?…)

Give a group a name and read it by name instead of by number:

Matches5/5 as expected
/^(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})$/

Should match2/2

  • 2026-10-13
  • 1999-12-31

Should not3/3

  • 26-10-13
  • 2026/10/13
  • 2026-1-3
Try it in the Playground
The same date pattern, now with each part named. It matches the same lines as the numbered version.
const { year, month, day } =
  "2026-10-13".match(/(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/).groups;

year;  // "2026"

Named groups still get numbers too, so m[1] is the year either way. The names just mean the code survives edits to the pattern.

They work in replacements as well, with $<name>:

"2026-10-13".replace(
  /(?<y>\d{4})-(?<m>\d{2})-(?<d>\d{2})/,
  "$<d>/$<m>/$<y>",
);  // "13/10/2026"

04Referring back: \k

A backreference matches the same text a group matched earlier. With named groups, it's \k<name> instead of \1:

Matches4/4 as expected
/^(?<quote>["']).*\k<quote>$/

Should match2/2

  • "hello"
  • 'hello'

Should not2/2

  • "hello'
  • 'hello"
Try it in the Playground
The closing quote has to be the same character as the opening one.

That says what it means in a way \1 never does, and it still works if you add groups in front.

05Python spells it differently

Python had named groups first, with a P:

import re

m = re.match(r"(?P<year>\d{4})-(?P<month>\d{2})", "2026-10")
m.group("year")      # "2026"
m.groupdict()        # {"year": "2026", "month": "10"}

re.sub(r"(?P<y>\d{4})-(?P<m>\d{2})", r"\g<m>/\g<y>", "2026-10")  # "10/2026"
re.match(r"(?P<q>[\"']).*(?P=q)$", '"hi"')  # backreference by name

So the same idea is (?<name>…) and \k<name> in JavaScript, but (?P<name>…), (?P=name) and \g<name> in Python. Non-capturing (?:…) is the same everywhere.

06Groups that never repeat-capture

One more surprise: a group inside a quantifier only keeps its last repetition.

"abc".match(/(\w)+/)[1];  // "c", not ["a", "b", "c"]

If you need every repetition, use the g flag with matchAll (or re.findall) on the inner pattern instead.

07Try one

The quotes have to match. Use a group and a reference back to it:

Live huntpar 12

Catch text wrapped in matching quotes: "…" or '…'.

Targets · 0/5 caught

  • "hi"target
  • 'hi'target
  • "it's"target
  • 'say "no"'target
  • ""target

Innocents · 0 hit

  • "hi'innocent
  • 'hi"innocent
  • hiinnocent
  • "hiinnocent
  • hi"innocent

Type a regex — the board updates as you go.

Try it in the Playground
Like this? A new one drops every day.Play today’s hunt

08Checklist

  • ( … ) groups and captures. (?: … ) only groups.
  • Use (?: … ) for every group you don't read back.
  • (?<name> … ) captures by name. Read it with .groups.name, replace with $<name>.
  • \k<name> matches the same text again.
  • Python: (?P<name> … ), (?P=name) and \g<name>.
  • A repeated group keeps only its last repetition.

09Keep practising