skip to content

In a regular expression with nested capture groups, how are the group numbers assigned?

level: middleimportance: should knowfreq 48%

answer

  1. count the opening parentheses
  2. left to right, outer before inner
  3. group 0 is the whole match
  4. `(?:...)` takes no number
  5. a repeat keeps only the last iteration

basics

~20 s

Groups are numbered by the left-to-right order of their opening parentheses, so an outer group always has a lower number than the groups nested inside it. Group 0 is the whole match, and non-capturing groups take no number.

solid answer

~40 s

Count opening parentheses from the left: the first capturing `(` is group 1, the second is group 2, and so on. Nesting therefore numbers **outside in** — in `((\w+)=(\w+))` applied to `user=alice`, group 1 is `user=alice`, group 2 is `user` and group 3 is `alice`. By convention group 0 denotes the entire match. A non-capturing group `(?:...)` groups for precedence without taking a number, so inserting one never renumbers anything after it; a named group normally carries a number as well as its name. Numbering is purely positional — it is decided by the pattern text, not by which branches actually participated in the match.

go deeper

for a junior

Be able to count parentheses and name which group holds which piece. Know that group 0 refers to the whole match and that not every pair of parentheses captures.

for a middle

Explain that numbering is positional and outside-in for nested groups, and that a group inside a repeat keeps only its last iteration, so a list pattern cannot hand back its elements.

for a senior

Show the maintenance angle: unnamed groups make a pattern fragile under edits, and an unset group is a different fact from an empty capture — code that conflates them turns a missing field into a blank one.

for a principal

Treat the extracted shape as an interface. If downstream code reads positional groups, the pattern is a schema that anyone editing it can break silently, and naming or a structured extraction step is the cheaper long-term contract.

## Numbering follows the opening parenthesis The rule is short and entirely syntactic: **scan the pattern left to right and number each capturing `(` as you meet it**. The number is fixed by the text of the pattern, before any input is seen, and it does not depend on which alternatives end up participating. The consequence people trip over is nesting. Because the *opening* parenthesis decides, an outer group is always numbered before the groups it contains — numbering runs outside in, not inside out. ## A worked example Pattern `((\w+)=(\w+))` against the field `user=alice`: | group | how it is reached | text after the match | |---|---|---| | 0 | the whole match, by convention | `user=alice` | | 1 | first `(` — the outer group | `user=alice` | | 2 | second `(` — nested, before `=` | `user` | | 3 | third `(` — nested, after `=` | `alice` | Read it by counting parentheses in the pattern, never by counting the interesting pieces in the output. A weak answer reports `user` as group 1 because it is the first *interesting* value; the outer group came first in the text and took the number. ## Groups that take no number - `(?:...)` — a **non-capturing group**. It exists for precedence and for applying a quantifier to a sequence. It stores nothing and takes no number, so adding or removing one never renumbers the groups after it. - Lookahead and lookbehind constructs likewise group without capturing, though any capturing group written *inside* them does take a number. - A **named group** normally has both a name and a number; the name is an alias, not a replacement, so mixing named and unnamed groups still shifts the positional numbers. This is why patterns that are edited often are better written with non-capturing groups everywhere except the pieces you actually read: every stray pair of parentheses added for precedence otherwise renumbers everything downstream, and the code that reads group 3 silently starts reading something else. ## A capturing group inside a repeat What happens when a capturing group is itself repeated? Each iteration overwrites the group's contents, so once the match finishes the group holds the text of its **last successful iteration** — that is the usual convention, and the alternative of accumulating every iteration is rare enough that you should not assume it. For `(?:(\d+),)+` against `1,2,3,`: 1. The repeat runs three times. 2. Group 1 captures `1`, then `2`, then `3`, each overwriting the previous value. 3. After the match, group 1 holds `3` — the other two are gone. So a pattern shaped like this can tell you *that* a comma-separated list matched and give you the whole span, but it cannot hand back the items. To get every element you match the list once as a whole and split it, or you iterate matches of the element pattern across the span. This is one of the most common surprises in real extraction code. ## Participated, unset, and empty A group in a branch that was never taken — `(a)|(b)` where the second alternative won — did not participate. Conventions differ on how that is reported: some distinguish an unset group from one that matched empty text, others surface both as an empty value. Since a group *can* legitimately match the empty string (`(x*)` against `y`), "empty" and "absent" are genuinely different facts, and code that treats them as the same silently turns a missing optional field into a present blank one. Where the distinction matters, check participation explicitly rather than inferring it from the text. ## Practical habits - Count parentheses, not values, when you are working out a number. - Use non-capturing groups for everything you do not read, so numbering is stable under edits. - Prefer names over numbers in any pattern long enough to need a comment. - Never expect a repeated group to hand back its history; restructure the extraction instead. - Treat an optional group's absence as a distinct case from an empty capture.

  • After a capturing group inside a repeat matches several times, what does it hold?
    By the usual convention, the text of its last successful iteration — each pass overwrites the previous one. `(?:(\d+),)+` over `1,2,3,` leaves `3` in group 1. To recover every element, capture the list as one span and split it, or iterate matches of the element pattern across that span.
  • Why do experienced authors write `(?:...)` for groups they never read?
    Because numbering is positional. Any capturing parenthesis added for precedence shifts every later group number, and the code reading group 3 then reads something else without failing. Non-capturing groups keep the numbering stable under edits and also keep the reader's intent visible.

saying these in an interview costs you the question

  • Numbers groups by closing parenthesis, so inner groups come first
  • Thinks a non-capturing group still shifts later group numbers
  • Expects a repeated group to hold every iteration it matched
  • Says group 1 is the whole match
  • Never checks whether an optional group participated at all