What do atomic groups `(?>...)` and possessive quantifiers do in Python's `re`?
answer
- Take it and never give it back
- Saved alternatives are thrown away
- Shipped in one 3.x release, not earlier
- Shorthand form is a plus after the quantifier
- Can change what the pattern accepts
basics
~10 sBoth tell the engine to throw away the backtracking alternatives once the enclosed part has matched. Added in Python 3.11, they collapse an exponential search and can turn a would-be match into a non-match.
solid answer
~40 sAn atomic group `(?>...)` matches its contents and then discards every saved alternative inside it: the engine may fail past the group, but it may never re-enter it to try a different split. A possessive quantifier — `*+`, `++`, `?+`, `{m,n}+` — is the same idea applied to a single repeat: take as much as possible and never give any back. Python's `re` gained both in **3.11**; on 3.10 and earlier `a*+` is a syntax error and `(?>...)` is unknown extension syntax. They are the direct cure for catastrophic backtracking, because the exponential search space is exactly the set of alternatives they delete. They also change semantics: `re.fullmatch(r'a*+a', 'aaa')` fails where `a*a` succeeds, because `a*+` keeps all three `a`s and refuses to hand one back.
code
python · 14 linesimport re
import time
subject = "a" * 22 + "!"
start = time.perf_counter()
re.search(r"(a+)+$", subject)
nested = time.perf_counter() - start
start = time.perf_counter()
re.search(r"(?>(a+)+)$", subject)
atomic = time.perf_counter() - start
print(f"nested {nested:.3f}s vs atomic {atomic:.6f}s")go deeper
Recall that (?>...) and a*+ mean 'match this and never reconsider it', and that they are a relatively recent addition to re rather than long-standing syntax.
Explain that they delete the saved backtracking alternatives, name Python 3.11 as the release that added them, and show a case where a possessive repeat turns a match into a non-match.
Judge when to use one instead of rewriting the pattern, and check the change against inputs that previously matched. Know that they bound the search, not the wall-clock time.
Weigh the portability cost of pinning patterns to 3.11+ syntax when pattern strings are shared across services or stored in config, against the maintenance cost of hand-rewritten unambiguous patterns.
## The mechanism Every time a quantifier in a backtracking engine takes a repetition, it records the option of having taken fewer. Those recorded options are the search space, and on an ambiguous pattern they are what goes exponential. **Atomic grouping** removes them. `(?>...)` matches its body normally, but the moment the body succeeds, the engine drops every backtracking state created inside the group. If matching later fails further along the pattern, the engine cannot step back into the group to try a different way of matching it; the whole group fails as a unit and the failure propagates outward. **Possessive quantifiers** are shorthand for the same thing over a single repeat: `X*+` is exactly `(?>X*)`, and likewise `X++`, `X?+` and `X{m,n}+`. Take the maximum, keep it, never give any back. ## Version facts Both arrived in **Python 3.11**. On 3.10 and earlier, `re.compile(r"a*+")` raises `re.error` ("multiple repeat") and `(?>` is rejected as an unknown extension. So a pattern using them is not portable to an older interpreter, which matters if the pattern string travels — a config file, a database column, a shared rules file read by more than one service. There is no runtime feature flag to check; you are checking the interpreter version. ## What it does to catastrophic backtracking Against `'a'*22 + '!'`, `(a+)+$` explores every partition of the run and takes a fraction of a second — at 40 characters it is effectively a hang. `(?>(a+)+)$` returns in microseconds: the group swallows all the `a`s on its first greedy pass, the alternatives are discarded, `$` fails at the `!`, and there is nothing to backtrack into, so the attempt at that offset ends immediately. The same holds for `(a+)++$`. The exponent is gone, because the states that made it were never kept. ## The semantic catch Atomic constructs are not a transparent optimisation. They can change what the pattern matches. ```pycon >>> import re >>> bool(re.fullmatch(r"a*a", "aaa")) True >>> bool(re.fullmatch(r"a*+a", "aaa")) False ``` `a*a` matches because `a*` greedily takes three `a`s, fails to find a fourth for the trailing `a`, backs off to two and succeeds. `a*+` also takes three but refuses to back off, so the trailing `a` has nothing left and the whole match fails. The rule of thumb: an atomic construct is safe when the thing that follows it can never match the same characters the atomic part consumed. `\d++,` is fine because a comma is not a digit; `\w*+s` is a trap because `s` is a word character. ## Where each fix belongs Prefer **removing the ambiguity** — `(a+)+` collapsing to `a+`, `(\s*\w+)*` rewritten as `\w+(\s+\w+)*`, alternation branches made disjoint. That version is portable, faster still, and easier to read, and it usually reveals that the original pattern was expressing something confused. Reach for atomic grouping when the ambiguity is genuinely wanted or when the rewrite is not obvious — a hand-maintained pattern with many alternation branches, or one where you want to state explicitly "once this prefix is consumed, do not reconsider it". It is also useful as a targeted fence around one subpattern inside a larger expression you do not want to redesign. ## What it does not give you Atomic groups bound the *search*, not the *runtime*. A pattern can still be quadratic (`re.search()` retrying at every offset over a long string), and `re` still has no timeout of any kind, so a pattern coming from outside your codebase is not made safe by wrapping it. For untrusted patterns the only hard bound is a process you can kill. Finally, `(?>...)` is non-capturing. If you need the text, capture inside it — `(?>(\d+))` — or capture the atomic group's span with a surrounding group.
- When would an atomic group silently break a pattern that previously worked?Whenever the construct that follows can match the same characters the atomic part consumed. `\w*+s` never matches `words`, because `\w*+` takes the final `s` and will not hand it back. The safe shape is an atomic part followed by something disjoint from it — digits then a separator, word characters then punctuation. Review any atomic change against inputs that used to match, not just the slow one.
- Which fix would you prefer over an atomic group, and why?Rewriting the pattern so the ambiguity is not there: `(a+)+` becomes `a+`, `(\s*\w+)*` becomes `\w+(\s+\w+)*`, overlapping alternation branches are made disjoint. It runs at least as fast, works on every Python version, does not change match semantics by accident, and it usually exposes that the original pattern was unclear about what it wanted.
saying these in an interview costs you the question
- Thinks `(?>...)` is just a non-capturing group
- Assumes atomic grouping never changes what matches
- Believes possessive quantifiers have always been in `re`
- Claims they give the match a time limit
- Reaches for them before trying the unambiguous rewrite