Quantifiers: Greedy, Lazy and Possessive
Repeating parts of a pattern with *, +, ? and {n,m}, why quantifiers are greedy by default, how lazy and possessive versions behave, and the classic overmatching mistakes.
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".
.+ 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.