R

ReflexKR

Reflex Docs

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

Regular expressions you can see
Lesson 9

Time 6:40–7:30

The safe's rules: lookaround

CASE-009

Grep & Co. Data Detective Agency

Case file

The safe's rules

Client

Director Han Byeol

Evidence

The agency's old safe, six candidates for a new password, and the ledger inside the safe.

To close the case

Check the new password rules (at least 8 characters, with at least one digit, one lowercase letter and one special character) with a single pattern, and add thousands separators to the amounts in the ledger.

The director crouches in front of the old safe in the corner. "The Backtracker brought up the safe just to scare us. Still, we're changing the password today. And this time we're following the rules properly."

The new password has three rules, in any order: 1) at least 8 characters; 2) at least one digit, one lowercase letter and one special character; 3) all of it checked with one single regex. The director has jotted them down in Korean (규칙 = rule, 후보 = candidates):

규칙 1. 8자 이상
규칙 2. 숫자, 소문자, 특수문자가 각각 하나 이상
규칙 3. 이 모든 걸 정규식 하나로 검사한다

후보:
vault2026!
vault2026
VAULT2026!
v!2
longpassword!
safe#2026box

You remember the words on the card: "If you can check them all from the same spot." That's exactly the syntax you'll learn today.

9-1. Syntax that checks a position, not a character (6 min)

Think back to ^, $ and \b from Lesson 4. They didn't eat a single character. They only checked a position: "is this the start?", "is this a word boundary?". A match like that is called a zero-width match.

re.findall(r"\b", "hi yo")      # ['', '', '', '']
re.sub(r"\b", "|", "hi yo")     # '|hi| |yo|'

What \b found was four empty strings, in other words four positions. Lookaround, today's topic, lets you turn that kind of position check into any condition you want.

9-2. Check what comes next: (?=...) (8 min)

X(?=Y) finds "an X that is followed by Y". Y is only checked and isn't included in the match. This is called a lookahead.

re.findall(r"\d+(?=원)", "3000원짜리 5개, 합계 15000원")   # ['3000', '15000']

The text is a Korean receipt line: "3,000-won items, 5 of them, total 15,000 won" (원 is won, the Korean currency, and 개 counts items). The 5 in 5개 is left out because there's no 원 after it, and the numbers found don't include 원. It's a perfect fit when you want "numbers that end in won" but only need the numbers.

Evidence lab

re.search(r"", text)

Return value

\d+ on its own also catches the item count 5. Add a lookahead so only numbers followed by 원 are left.

9-3. What must not come next: (?!...) (6 min)

X(?!Y) is the opposite: "an X that is not followed by Y". It's called a negative lookahead.

re.findall(r"\b(?!tmp)\w+", "tmp_log report tmpfile final")   # ['report', 'final']
re.findall(r"\d{3}(?!\d)", "12345 678")                       # ['345', '678']
  • The first line finds "words that don't start with tmp". Right after a word boundary, it checks that tmp isn't there.
  • The second line finds "three digits with no more digits after them", that is, the last three digits of each run of numbers.

9-4. For several rules, chain lookaheads (8 min)

For conditions like the password rules, "it must have this, and that, and that too", put lookaheads side by side.

pw = r"(?=.*\d)(?=.*[a-z])(?=.*[^\w\s]).{8,}"
PieceWhat it checks
(?=.*\d)There's a digit somewhere
(?=.*[a-z])There's a lowercase letter somewhere
(?=.*[^\w\s])There's a special character somewhere (not a letter, digit, underscore or whitespace)
.{8,}Actually eats 8 or more characters

All three lookaheads check from the same spot (the very start). They don't eat characters, so when one finishes, the position hasn't moved, and the next condition starts from the start again. That's why chaining them means "AND".

cands = ["vault2026!", "vault2026", "VAULT2026!", "v!2", "longpassword!", "safe#2026box"]
[c for c in cands if re.fullmatch(pw, c)]   # ['vault2026!', 'safe#2026box']

Evidence lab

re.search(r"", text)

Return value

.{8,} only looks at the length. Put the digit, lowercase and special-character conditions in front as lookaheads.

Dot's tip

A lookahead just peeks ahead without moving its feet. Same as me: I sit on the windowsill and look outside, but I never climb over. So no matter how many times I peek, I'm still right where I started.

9-5. What came before: (?<=...) (?<!...) (8 min)

Flip the direction and you get a lookbehind. (?<=Y)X is "an X preceded by Y", and (?<!Y)X is "an X not preceded by Y".

re.findall(r"(?<=\$)\d+", "$30 and 40 and $5")      # ['30', '5']
re.findall(r"(?<!\$)\b\d+", "$30 and 40 and $5")    # ['40']

Lookbehinds come with one restriction. In Python's re, the length to look back over must be fixed.

re.compile(r"(?<=a+)b")      # PatternError: look-behind requires fixed-width pattern
re.compile(r"(?<=ab|cd)e")   # fine: both alternatives are 2 characters long
re.compile(r"(?<=a|bc)d")    # PatternError: look-behind requires fixed-width pattern

Several alternatives are fine as long as they're all the same length. If the lengths differ, split the lookbehind in two and group them separately, like (?:(?<=a)|(?<=bc))d.

Evidence lab

re.search(r"", text)

Return value

\$\d+ pulls out the $ too. Move the \$ into a lookbehind so the return value becomes ['30', '5'].

9-6. The safe's ledger: thousands separators (4 min)

The numbers in the ledger inside the safe are run together, like 1234567, which makes them hard to read. Find the positions where commas belong and put a comma there. Finding positions is exactly what lookaround is for.

re.sub(r"(?<=\d)(?=(?:\d{3})+(?!\d))", ",", "1234567 and 89 and 1000")
# '1,234,567 and 89 and 1,000'

Here's how to read it: "a position with a digit before it ((?<=\d)), followed by one or more groups of three digits ((?:\d{3})+), after which the digits end ((?!\d))." That position isn't a character, so sub doesn't delete anything; it just slips in a comma.

Put a group inside a lookahead and you can even find overlapping matches. The trick is to move the position one step at a time and pull out the two characters at each spot with the group.

re.findall(r"(?=(\w\w))", "abc")   # ['ab', 'bc']

Evidence lab

re.search(r"", text)

Return value

The replacement is a single comma. The pattern has to find positions, not characters, or the digits will disappear.

Cracking the case (10 min)

Finish the checker and filter the candidates. The agency's tool prints its verdict in Korean: 통과 = pass, 탈락 = fail.

pw = re.compile(r"(?=.*\d)(?=.*[a-z])(?=.*[^\w\s]).{8,}")
for c in cands:
    print(f"{c:<14} {'통과' if pw.fullmatch(c) else '탈락'}")
# vault2026!     통과
# vault2026      탈락
# VAULT2026!     탈락
# v!2            탈락
# longpassword!  탈락
# safe#2026box   통과

The director picks neither of the two that passed. "The moment it's written on a candidate list, it's not a password anymore." Then she sets a new password that follows the rules herself, where nobody can see, and puts only the checker in the agency's shared toolbox.

Case closed report — Built a tool that checks the safe's password rules with three lookaheads. Only two of the six candidates passed. The director changed the password separately. Tidied up the ledger by adding thousands separators to the amounts.

Exercises

  1. Add "at least one uppercase letter" to the rules. Which candidates survive?
  2. From "price: 1200won, tax: 120won", pull out only the numbers in front of won using a lookahead.
  3. Predict the result of re.findall(r"(?<!\d)\d{2}(?!\d)", "1 22 333 44"). Why does no part of 333 show up?

The Backtracker's trail

(a+)+$

I'll leave your safe alone. Instead, I've sent three letters to Detective Kang at the police station. Can you prove they were written by the same person? Habits leave their mark in the letters.

The director's notebook

  • Lookaround is a zero-width match. It eats no characters and only checks a position (the same family as ^ $ \b).

  • X(?=Y): an X followed by Y. X(?!Y): an X not followed by Y.

  • (?<=Y)X: an X preceded by Y. (?<!Y)X: an X not preceded by Y. Lookbehinds must be fixed-width.

  • Chained lookaheads mean AND: (?=.*\d)(?=.*[a-z])(?=.*[^\w\s]).{8,}

  • sub on a position inserts without deleting anything: thousands separators.

  • A group inside a lookahead, like (?=(\w\w)), pulls out overlapping matches.

← Previous

Lesson 8 · The Backtracker's challenge: greedy, lazy and possessive

Next →

Lesson 10 · Same modus operandi: backreferences and conditionals

Get news about new lessons

We'll email you when this course gets new lessons or a follow-up course opens.

We collect: your email address · Purpose: news about new lessons or the launch of this course · Kept until: you ask us to remove it. We never use it for anything else or share it. Reply to any notice email and we'll delete it right away.

Questions & answers

Ask anything about this lesson. Questions from every language version of this page appear together.

Shown as plain text, without links or formatting.

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
한국어English日本語简体中文EspañolDeutsch

© 2026 ReflexKR · Since 2026 · CC BY-NC 4.0

Built with Reflex