Fix re.findall reporting a group that did not match - #8563
Conversation
findall returns the matched text rather than a match object, so a group that
took no part in the match is reported as an empty value. With one group it
was reported as `None`:
>>> re.findall(r"(a)?b", "b ab")
[None, 'a'] # CPython: ['', 'a']
The branch for two or more groups was already right, since it passes `""` to
`Match.groups` as the default, so the two halves of the same function
disagreed with each other.
That default is built as a `str` whatever the pattern is, so a `bytes`
pattern came back with `str` mixed into it:
>>> re.findall(rb"(a)|(b)", b"ab")
[(b'a', ''), ('', b'b')] # CPython: [(b'a', b''), (b'', b'b')]
The empty value is now built once from `isbytes` and both branches use it.
`Match.groups` still reports `None`, which is what CPython does.
Assisted-by: Claude Code:claude-opus-5
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Included review availability: Your plan provides up to 10 included reviews per hour; 7 remain after this review. 📝 WalkthroughWalkthrough
ChangesPattern
Estimated code review effort: 2 (Simple) | ~10 minutes Merge Risk: ⚪ Minimal · up to The change corrects Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
youknowone
left a comment
There was a problem hiding this comment.
Thanks! I am surprised this was not covered by test_re
re.findallhands back the matched text rather than a match object, so a group that took no part in the match is reported as an empty string. With one group it was reported asNone:Anything reading the result as a list of strings gets a
TypeErrorfrom"".join(...), frommax(..., key=len), from a.strip()in a loop. With two groups it was already right, because that branch passes""toMatch.groupsas the default while the one-group branch fell through toNone. The two halves of the same function disagreed.While pinning that down, the same line turned up a second one. The default is built as a
strwhatever the pattern is, so abytespattern comes back withstrmixed into it:That one is on the two-group branch, so it is there on
maintoday and is not a consequence of the first fix. Both come from the same decision, so the empty value is now built once fromzelf.isbytes, using the idiomsub_implalready uses a few lines up, and both branches take it.Match.groups()keeps reportingNone, which is what CPython does and is the reason the two are easy to confuse.re.splitkeepsNoneas well. Both are covered by the tests so a later change cannot quietly pull them along.Checked against CPython 3.14.7 over 41 cases: one group participating and not, named groups, zero groups, two and three groups,
strandbytes, the compiled-pattern form, and the neighbours that share the match machinery (finditer,group,groups,groupdict,split,sub,expand). All 41 agree now. Without the change 20 of them differ.Lib/test/test_re.py::test_re_findallonly uses groups that participate, or two groups, so the suite passes either way. It still passes here, 166 tests. The new cases are inextra_tests/snippets/stdlib_re.py, and they also assert the result types, since that is where thebyteshalf went wrong.Summary by CodeRabbit
Bug Fixes
re.findallso unmatched capturing groups consistently return an empty value matching the pattern type:""for text patterns andb""for byte patterns.Tests