Repeating with quantifiers

A quantifier says how many times the item directly before it may occur. That item is one character, one character class or one group in parentheses.

Quantifier Means Example Matches
* Zero or more times ab*c ac, abc, abbbc
+ One or more times [0-9]+ 7, 42, 31877
? Zero or one time (optional) https? http, https
{n} Exactly n times INC-[0-9]{5} INC-31877
{n,} At least n times [0-9]{4,} 2026, 318400
{n,m} Between n and m times [0-9]{1,3} 1, 42, 255

Because a quantifier applies to one item only, ab+ means "a, then one or more b", not "ab repeated". To repeat several characters, put them in a group: (ab)+, or (?:ab)+ if you do not need to capture it (see groups).

import re

print(re.findall(r"ab+", "abbb abab"))
print(re.findall(r"(?:ab)+", "abbb abab"))
print(re.findall(r"https?://", "http:// https:// htps://"))
print(re.findall(r"KE-[0-9]{4}\b", "KE-0042 KE-42 KE-00420"))
['abbb', 'ab', 'ab']
['ab', 'abab']
['http://', 'https://']
['KE-0042']

Braces differ between tools

The braces are where flavors disagree most. In grep's and sed's basic syntax, {2} is literal text and the quantifier is written \{2\}; with -E it is {2} as everywhere else (see basic and extended syntax). A missing lower bound, {,3}, is accepted by Python as "0 to 3 times", but JavaScript without the u flag and PHP 8.2 (PCRE2 10.42) treat it as the literal text {,3}, and JavaScript with u rejects it. Write {0,3} and it means the same everywhere.

printf '%s\n' 'port 22' 'port 443' 'port 8443' 'port 4{3}' > ports.txt
echo "--- basic syntax, '4{3}' (literal braces):"
grep '4{3}' ports.txt
echo "--- basic syntax, '[0-9]\{4\}' (a quantifier):"
grep '[0-9]\{4\}' ports.txt
echo "--- extended syntax, '[0-9]{4}':"
grep -E '[0-9]{4}' ports.txt
--- basic syntax, '4{3}' (literal braces):
port 4{3}
--- basic syntax, '[0-9]\{4\}' (a quantifier):
port 8443
--- extended syntax, '[0-9]{4}':
port 8443

Greedy by default

A greedy quantifier takes as many repetitions as it can, and gives some back only if the rest of the pattern cannot match otherwise. All quantifiers in the table above are greedy. Usually that is what you want: [0-9]+ should take the whole number 31877, not just 3. It goes wrong when the repeated item can also match the text that should come after it, and . can match almost anything:

import re

html = "<td>KE-0042</td><td>318.4</td>"
print(re.findall(r"<.+>", html))

line = "user=amara.okafor action=login result=fail"
print(re.search(r"user=(.*) ", line).group(1))
['<td>KE-0042</td><td>318.4</td>']
amara.okafor action=login

The first pattern was meant to find tags, but .+ ran to the last > on the line. The second was meant to take the user name up to the next space, but .* took everything up to the last space. Both are cases of overmatching: the pattern matches more text than intended.

How backtracking works

Why does <.+> still end with a > if .+ swallowed the whole line? The answer is backtracking: when the rest of a pattern fails, the engine goes back to the most recent choice it made, such as how many characters a quantifier took, tries the next option, and continues. A greedy quantifier's next option is "one character fewer".

Matching <.+> and <.+?> against the text <a><b>. Greedy: .+ first takes all five characters after the opening bracket, the final > finds nothing, so .+ gives one character back and the match is the whole text. Lazy: .+? takes one character, > matches next, and the match is <a>. text < a > < b > greedy, try 1 < .+ > end of text: fail greedy, try 2 < .+ > give back 1: match <a><b> lazy .+? < .+? > take 1: match <a>
Greedy .+ grabs everything and backs off one character at a time until > fits. Lazy .+? starts with one character and stops at the first >.

Backtracking is what makes a pattern such as [0-9]+0 work on "100": [0-9]+ first takes all three digits, the 0 then fails at the end of the text, so the quantifier gives back the last digit and the 0 matches it. Engines that work this way are called backtracking engines; Python, JavaScript, PHP's PCRE2, Java and .NET (by default; .NET 7 and later also offer a non-backtracking option) all are. Each step back is cheap, but in badly written patterns the number of combinations to try can explode. That is the subject of performance and catastrophic backtracking.

Lazy quantifiers

A lazy quantifier (also called non-greedy or reluctant) takes as few repetitions as possible and takes one more only when the rest of the pattern fails. You make a quantifier lazy by adding a question mark: *?, +?, ??, {n,m}?.

import re

html = "<td>KE-0042</td><td>318.4</td>"
print(re.findall(r"<.+?>", html))

line = "user=amara.okafor action=login result=fail"
print(re.search(r"user=(.*?) ", line).group(1))
print(re.findall(r"[0-9]{2,}?", "12345"))   # lazy: as few as allowed, 2
['<td>', '</td>', '<td>', '</td>']
amara.okafor
['12', '34']

JavaScript behaves the same. PHP's PCRE2 does too, and it also has a U modifier that flips the default: with it, every quantifier is lazy and a trailing ? makes it greedy. That makes a pattern harder to read for anyone who misses the modifier, so use it sparingly.

const html = '<td>KE-0042</td><td>318.4</td>';
console.log(html.match(/<.+>/g));
console.log(html.match(/<.+?>/g));
[ '<td>KE-0042</td><td>318.4</td>' ]
[ '<td>', '</td>', '<td>', '</td>' ]
<?php
$html = '<td>KE-0042</td><td>318.4</td>';
echo preg_replace('/<.+?>/', '|', $html), "\n";   // lazy
echo preg_replace('/<.+>/U', '|', $html), "\n";   // U modifier: lazy by default
echo preg_replace('/<.+>/', '|', $html), "\n";    // greedy
|KE-0042||318.4|
|KE-0042||318.4|
|

The POSIX syntaxes of grep, sed and awk have no lazy quantifiers. In grep -E, .+? simply means ".+, optionally", which is still greedy. You need grep -P for the lazy version, or, better, a negated class:

echo '<td>KE-0042</td><td>318.4</td>' > row.html
echo "--- grep -oE '<.+?>' (no lazy quantifiers in ERE):"
grep -oE '<.+?>' row.html
echo "--- grep -oP '<.+?>':"
grep -oP '<.+?>' row.html
echo "--- grep -oE '<[^>]+>' (negated class):"
grep -oE '<[^>]+>' row.html
--- grep -oE '<.+?>' (no lazy quantifiers in ERE):
<td>KE-0042</td><td>318.4</td>
--- grep -oP '<.+?>':
<td>
</td>
<td>
</td>
--- grep -oE '<[^>]+>' (negated class):
<td>
</td>
<td>
</td>

Possessive quantifiers and atomic groups

A possessive quantifier is greedy and never gives anything back: once it has taken its characters, backtracking into it is not allowed. You write it with an extra plus: *+, ++, ?+, {n,m}+. An atomic group, (?>…), does the same for a whole group: as soon as the group has matched, the engine forgets the other ways it could have matched. a++ and (?>a+) mean the same thing.

That changes results only when giving back would have helped. [0-9]+0 matches "100" thanks to backtracking; [0-9]++0 cannot, because the digits are never released:

import re   # possessive and atomic: Python 3.11 and later

print(re.fullmatch(r"[0-9]+0", "100"))
print(re.fullmatch(r"[0-9]++0", "100"))
print(re.fullmatch(r"(?>[0-9]+)0", "100"))
print(re.findall(r'"[^"]*+"', 'meter="KE-0042" status="ok"'))
<re.Match object; span=(0, 3), match='100'>
None
None
['"KE-0042"', '"ok"']

So why use them? Mainly to fail fast. In "[^"]*+", giving back characters can never help (a non-quote is never a quote), so the possessive version tells the engine not to waste time trying. In patterns that would otherwise backtrack catastrophically, an atomic group or possessive quantifier can turn seconds into microseconds; the page on catastrophic backtracking shows how.

PHP's PCRE2 (and grep -P) has had both for a long time:

<?php
var_dump(preg_match('/^[0-9]+0$/', '100'));
var_dump(preg_match('/^[0-9]++0$/', '100'));
var_dump(preg_match('/^(?>[0-9]+)0$/', '100'));
int(1)
int(0)
int(0)

JavaScript has neither, up to and including ECMAScript 2026. A possessive quantifier is a syntax error there:

const pattern = new RegExp('[0-9]++0');
console.log(pattern.test('100'));
SyntaxError: Invalid regular expression: /[0-9]++0/: Nothing to repeat

Classic mistakes

Using .* for "something here"

.* means "anything, as much as possible", so it crosses field boundaries, quotes and separators. Say what the field actually contains instead: \S+ (no spaces), [^,]* (up to the next comma), [0-9]+ (digits). In a search, a .* at the start or end is useless anyway: grep '.*ERROR.*' finds exactly the same lines as grep 'ERROR'.

Forgetting that * allows zero

* and ? can match nothing at all, and an empty match is still a match. \d* therefore "finds" an empty string at every position that is not followed by a digit. Use + when at least one character must be there.

import re

print(re.findall(r"\d*", "a12b"))
print(re.findall(r"\d+", "a12b"))
print(bool(re.fullmatch(r"INC-\d*", "INC-")))   # an empty ticket number passes
['', '12', '', '']
['12']
True

Matching inside longer values

[0-9]{1,3} finds "1 to 3 digits" and splits 8443 into 844 and 3, and 31877 into 318 and 77. A quantifier says how many, not where the value ends. Add anchors or word boundaries (\b[0-9]{1,3}\b), or validate the whole string with a full match.

const text = 'ports 22, 443 and 8443; ticket INC-31877';
console.log(text.match(/[0-9]{1,3}/g));
console.log(text.match(/\b[0-9]{1,3}\b/g));
[ '22', '443', '844', '3', '318', '77' ]
[ '22', '443' ]

Which engines support what

Engine Greedy Lazy *? Possessive *+ Atomic (?>…)
grep, sed, awk (POSIX basic and extended) Yes No No No
PCRE2 (PHP, grep -P) Yes Yes Yes Yes
Python re Yes Yes From 3.11 From 3.11
JavaScript (Node.js 24, browsers) Yes Yes No No
RE2 Yes Yes No No

RE2 (and engines built on the same idea, such as Rust's regex crate) does not need possessive quantifiers or atomic groups: it never backtracks, so there is nothing to switch off. The differences between all these engines are collected on the page on regex flavors.