Regular expressions you can see
·
Course overview
1
The missing payment: finding text as it is
2
The hit-and-run car: character classes
3
The torn phone book: quantifiers
4
The fake sign-ups: anchors and validation
5
Dates all over the place: groups and alternation
6
The 3 a.m. login: named groups and Match objects
7
The night before the story: substitution and splitting
8
The Backtracker's challenge: greedy, lazy and possessive
9
The safe's rules: lookaround
10
Same modus operandi: backreferences and conditionals
11
The unreadable regex: flags and compiling
12
Unmasking the Backtracker: review and a tokenizer
Time 5:50–6:40
CASE-008
Grep & Co. Data Detective Agency
Case file
Client
Left at the agency door (sender: the Backtracker)
Evidence
A black envelope. Inside, a card with one line of regex and a single question.
To close the case
Explain why the pattern on the card is slow, and reply with a pattern that finds the same strings but finishes right away.
In the morning there's a black envelope wedged in the office door. Inside is a single card. The director holds it up to the light, then sets it down on the desk. "So he finally says hello. With a signature instead of a face."
^(a|aa)+$
이 패턴은 정답을 맞힌다. 그런데 왜 이렇게 느린가?
답은 패턴으로 보내라.
(a+)+$The card is in Korean. It reads: "This pattern gets the right answer. So why is it so slow? Send your answer as a pattern." Below it, the signature.
"You can only solve this if you know how a regex engine thinks," the director says. "Today we look inside the engine's head."
A regex engine tries to fit the pattern onto the string one piece at a time, from left to right. When something doesn't fit, it doesn't give up right away. It undoes the choice it just made and tries another path. That's called backtracking.
In Lesson 3 you saw that quantifiers, by default, "eat as much as they can" (they're greedy). Here's what that does when you pull tags out of HTML.
html = "<b>bold</b> and <i>it</i>"
re.findall(r"<.+>", html) # ['<b>bold</b> and <i>it</i>']Instead of four tags, the whole line came back as one. Follow what the engine did:
< matches the first <..+ is greedy, so it eats everything to the end of the line.> has to match, but there are no characters left. Failure..+ gives back characters one at a time and tries again. The moment it gives back the final >, the match succeeds.The result runs "from the outermost < to the very last >". The engine isn't wrong; the pattern is.
Evidence lab
re.search(r"", text)
Return value
Put a ? after a quantifier and it becomes a lazy quantifier that eats as little as possible. It starts with the minimum and only eats one more character when the rest of the pattern doesn't fit.
| Greedy | Lazy | Tries first |
|---|---|---|
* | *? | 0 times |
+ | +? | 1 time |
? | ?? | 0 times |
{m,n} | {m,n}? | m times |
re.findall(r"<.+?>", html) # ['<b>', '</b>', '<i>', '</i>']
re.findall(r"a+?", "aaa") # ['a', 'a', 'a']
re.findall(r"a??", "a") # ['', 'a', '']
re.search(r"\d{2,4}?", "12345").group() # '12'A lazy quantifier changes "where it stops". That makes it handy for grabbing short things with a clear end marker, like tags.
Here are the ways to pull out tags side by side.
| Pattern | Result | Verdict |
|---|---|---|
<.+> | One match: the whole line | Greedy, so it eats too much |
<.+?> | Four tags | Stops lazily |
<[^>]+> | Four tags | Can't eat a > in the first place |
<[^>]+> uses the negated class from Lesson 2 and only eats "characters that aren't >". It almost never has to back up, so it's the fastest, and its meaning is the clearest. As the director puts it: "Precise beats lazy."
Dot's tip
Lazy quantifiers aren't magic. They only change where the match ends. When a match fails, the engine backs up just as many times as before. Make a slow pattern lazy and it's still slow.
Now look at the pattern on the card, ^(a|aa)+$. It finds "a whole string made of one or more a's". It means the same as ^a+$.
bool(re.fullmatch(r"^(a|aa)+$", "aaaa")) # True
bool(re.fullmatch(r"^a+$", "aaaa")) # TrueThe trouble starts when it meets a string that doesn't match. If there's a b at the end, as in aaaa…ab, the engine tries every way of splitting the a's into a and aa, one by one, before it admits failure. The number of ways to split them explodes as the a's get longer.
Here are times measured on the agency's computer (Python 3.14).
| Number of a's | ^(a|aa)+$ | ^a+$ |
|---|---|---|
| 16 | 0.25 ms | under 0.04 ms |
| 20 | 1.1 ms | under 0.01 ms |
| 24 | 7.2 ms | under 0.01 ms |
| 28 | 51 ms | under 0.01 ms |
Every four extra a's make it about seven times slower. At that rate, 40 a's would take well over 10 seconds. A repeat with overlapping choices like this is the classic shape that makes backtracking explode. A repeat inside a repeat, like (a+)+, is the same kind of thing.
Evidence lab
re.search(r"", text)
Return value
Since Python 3.11, putting a + after a quantifier makes it a possessive quantifier. Once it has eaten characters, it never gives them back, even if the rest of the pattern fails. It removes the paths to back up through altogether.
re.fullmatch(r"a*a", "aaa") # <re.Match object; span=(0, 3), match='aaa'>
re.fullmatch(r"a*+a", "aaa") # Nonea*+a never matches any string. a*+ takes every a and won't give any back, so there's nothing left for the final a to match. Turned around, that same property lets us fix the card's pattern.
re.search(r"^(?:a|aa)++$", "a" * 28 + "b") # None (finishes instantly)++ makes the group's repeat possessive, so on failure it gives up at once instead of trying other ways to split.
The atomic group (?>...) is also available since 3.11. Once the inside of the group has matched, that result is locked in as a whole, and the engine never backs up into it. A possessive quantifier is really shorthand for an atomic group: a++ is the same as (?>a+).
re.fullmatch(r"(?>a+)b", "aab") # <re.Match object; span=(0, 3), match='aab'>
re.fullmatch(r"(?>a+)a", "aaa") # NoneYou draft the reply with the director. There are three ways to fix a slow pattern.
^a+$ instead of ^(a|aa)+$.^(?:a|aa)++$, or an atomic group, ^(?>(?:a|aa)+)$.regex package has a timeout argument.)import time
text = "a" * 40 + "b"
start = time.perf_counter()
print(re.search(r"^a+$", text)) # None
print(time.perf_counter() - start < 0.01) # TrueThe new detective writes ^a+$ on the back of the card and slips it into the envelope. The director nods. "Short, readable, fast. A good reply."
Case closed report — Confirmed that the challenge pattern ^(a|aa)+$ takes exponentially longer to fail because of its overlapping choices. Replied with ^a+$, which means the same thing. Decided to keep the lab's time limit in place.
re.findall(r'".+"', 'say "hi" and "yo"'), then fix it to grab one quoted string at a time in two ways (a lazy quantifier and a negated class).(\d+)+$ is a dangerous shape, and write a safe pattern that means the same thing.a{2,4}? and a{2,4}+ each return when you run them with findall on "aaaaa"? Check in the Evidence lab.The Backtracker's trail
(a+)+$
Got your reply. The next puzzle is your agency's safe. Three password rules, in any order. That is, if you can check them all from the same spot.
The director's notebook
The engine tries from the left; when stuck, it undoes the choice it just made (backtracking).
* + ? {m,n} are greedy. Add ? after them to make them lazy: *? +? ?? {m,n}?
When the end is clear, as with tags, a negated class like <[^>]+> is the clearest and fastest.
Overlapping repeats like (a|aa)+ or (a+)+ blow up in time when they fail. Making them lazy doesn't help.
Possessive quantifiers *+ ++ ?+ {m,n}+ and atomic groups (?>...) never give back (3.11+). a++ is (?>a+).
For patterns from other people, limit the input length and run them somewhere with a time limit.
← Previous
Lesson 7 · The night before the story: substitution and splitting
Next →
Lesson 9 · The safe's rules: lookaround
Get news about new lessons
We'll email you when this course gets new lessons or a follow-up course opens.
R
ReflexKR
Learn Python and Reflex by doing. Every example was run and verified on Reflex 0.10.0.
ReflexKR is free, with no banner ads. If it helped you, you can support it with a coffee. ☕ Buy us a coffee
Affiliate disclosure© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0
Questions & answers
Ask anything about this lesson. Questions from every language version of this page appear together.