Alternation and grouping

Regular expressions · Essentials · 10 min

Grouping controls precedence; most confusing regex bugs come from | applying to more than intended.

The | operator means or: cat|dog matches either word. It has the lowest precedence of all regex operators, so it splits the entire pattern (or the enclosing group) into alternatives. ^cat|dog$ is "cat at the start, or dog at the end", not "the whole string is cat or dog".

Parentheses group things:

  • to limit alternation: ^(cat|dog)$, gr(a|e)y;
  • to apply a quantifier to a sequence: (ha)+, (\d{1,3}\.){3}\d{1,3};
  • to capture the matched text (next module).

If you only need grouping, not capturing, write a non-capturing group (?:...). It keeps capture numbering simple and avoids storing text you don't use. In Python, findall returns the groups instead of the whole match when the pattern contains capturing groups, which is another reason to prefer (?:...) for pure grouping.

Engines that backtrack try alternatives left to right and take the first that leads to an overall match, not the longest. So in|int against "integer" matches in. Put longer alternatives first, or add boundaries: \b(?:int|in)\b. POSIX tools like grep return the leftmost-longest match instead.

Example

import re

print(re.findall(r"cat|dog", "hotdog catalog"))           # ['dog', 'cat']
print(bool(re.search(r"^cat|dog$", "catalog")))           # True: ^cat matched
print(bool(re.search(r"^(?:cat|dog)$", "catalog")))       # False
print(re.findall(r"gr(?:a|e)y", "gray grey groy"))        # ['gray', 'grey']
print(re.findall(r"(?:ha)+", "hahaha!"))                  # ['hahaha']

print(re.search(r"in|int", "integer").group())            # in   (first alternative wins)
print(re.search(r"int|in", "integer").group())            # int

print(re.findall(r"(ha)+", "hahaha"))     # ['ha']  findall returns the group!

Remember

  • | has the lowest precedence: group alternatives with ( )
  • Use (?:...) when you need grouping but not capturing
  • Backtracking engines take the first alternative that works, not the longest

Watch out for

  • Writing ^yes|no$ for 'yes or no only' instead of ^(?:yes|no)$.

Check yourself

  1. Which strings does ^(?:jpg|png)$ match?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.python.org/3/howto/regex.html#grouping