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 2

Time 0:50–1:40

The hit-and-run car: character classes

CASE-002

Grep & Co. Data Detective Agency

Case file

The hit-and-run car

Client

Detective Kang, local precinct

Evidence

One witness statement and the entry/exit log of an underground car park. The witness saw only part of the number plate.

To close the case

Pull every number plate out of the car park log, and narrow it down to the one car that fits the witness statement.

Toward evening, Detective Kang pushes through the agency door, shaking off a wet umbrella. "Last night, around 22:45, hit-and-run at the exit of an underground car park. Witness didn't catch the whole plate." The director doesn't lift her hands from the keyboard. "Then write down what the witness did catch, as a pattern. Evidence leaves a pattern."

Korean number plates look like 12가3457: two digits, one Hangul syllable, four digits. Here is the witness statement, exactly as it was taken down at the precinct.

- 앞 두 자리는 12 아니면 17이었어요.
- 가운데 한글은 가, 거, 고 중 하나였어요.
- 마지막 숫자는 7이었어요.
- 차는 출구(OUT) 쪽으로 빠져나갔어요.

In English:

  • The first two digits were either 12 or 17.
  • The Hangul syllable in the middle was one of 가, 거 or 고 (ga, geo, go).
  • The last digit was 7.
  • The car left through the exit (OUT).

And here is the car park's entry/exit log. The camera sometimes misreads a plate.

2026-10-19 22:41 IN  B1 12가3457
2026-10-19 22:43 IN  B1 34나1207
2026-10-19 22:47 OUT B2 17거3407
2026-10-19 22:52 IN  B1 12-3457
2026-10-19 22:58 OUT B2 56다9081
2026-10-19 23:05 IN  B1 12a3457

Every line of the statement is a condition of the form "this position holds one of these". The character classes you learn today are exactly the syntax for writing that down.

2-1. Square brackets: one of these characters (5 min)

Square brackets [...] mean any one of the characters written inside. The witness said "one of 가, 거 or 고", so that position is written [가거고].

import re

re.findall("12[가거고]3457", "12가3457 12거3457 12나3457")   # ['12가3457', '12거3457']
re.findall(r"[ab]c", "ac bc cc")                            # ['ac', 'bc']
re.search(r"[abc]", "xyz")                                  # None
  • However many characters you put inside the brackets, they take up one position. [가거고] is not three characters but one.
  • Order doesn't matter. [가거고] and [고거가] are the same class.
  • If Lesson 1's . was "any one character", the brackets are "one of the characters I picked".

Evidence lab

re.search(r"", text)

Return value

Change the middle character to [가거고]. 가, 거 and 고 should pass, and 나 should be filtered out.

2-2. Ranges: [0-9], [a-z], [가-힣] (8 min)

Writing out all ten digits as [0123456789] is tedious. Inside brackets, a - between two characters makes a range. [0-9] is 0 through 9, and [a-z] is every lowercase English letter.

re.findall(r"[0-9]", "B1")            # ['1']
re.findall(r"[a-z0-9]", "B1 b2")      # ['1', 'b', '2']
re.findall(r"[a-zA-Z]", "a1Z")        # ['a', 'Z']
  • You can chain several ranges. [a-z0-9] is one lowercase letter or digit.
  • Ranges follow the order of code points (Unicode numbers). That's why [a-z] doesn't include capital letters.

In Unicode, the 11,172 precomposed Hangul syllables sit in one unbroken run, from 가 (U+AC00) to 힣 (U+D7A3). So a single [가-힣] catches any complete Hangul syllable.

print(hex(ord("가")), hex(ord("힣")))   # 0xac00 0xd7a3
re.findall(r"[가-힣]", "ㄱ ㅏ 가")       # ['가']

Standalone consonants and vowels (jamo) such as ㄱ and ㅏ live in a different block, so [가-힣] doesn't include them. The middle character of a number plate is always a complete syllable, so this range is all you need.

Write a range backwards and you get an error the moment the pattern is built.

re.compile("[a-Z]")   # re.PatternError: bad character range a-Z at position 1

Evidence lab

re.search(r"", text)

Return value

Replace the middle '가' with [가-힣] and find all four plates. The two lines the camera misread should be filtered out.

Dot's tip

A range only goes from the smaller code point to the bigger one. If you try to catch both cases at once with [a-Z], you get an error, because a is number 97 and Z is number 90, so it's backwards. Want upper and lower case? Use two ranges, like [a-zA-Z].

2-3. Negation: [^...] anything but these (7 min)

Put a ^ right at the front, just inside the bracket, and the meaning flips. [^0-9] is one character that is not a digit.

re.findall(r"[^0-9]", "12가34")     # ['가']
re.findall(r"[0-9^]", "2^3")        # ['2', '^', '3']
re.findall(r"[^0-9]", "1\n2")       # ['\n']
  • ^ means negation only when it comes first. Further back, as in [0-9^], it's just the ^ character.
  • [^0-9] still takes up one character. It's not "a spot with no digit" but "one character that isn't a digit".
  • A newline isn't a digit either, so [^0-9] catches it.

The car park camera sometimes misreads digits as letters: 1 as I, 5 as S, 2 as Z, and so on. Look for characters that are neither digits, Hangul nor spaces, and the misread spots give themselves away.

Dot's tip

I can't get past the window (a newline), but [^0-9] can. A newline counts as 'a character that isn't a digit'. Whenever you use [^...] on a multi-line log, check whether it's grabbing newlines too.

Evidence lab

re.search(r"", text)

Return value

[^0-9] catches Hangul and spaces too. Add 가-힣 and a single space inside the brackets so only the misread characters remain.

2-4. Shorthands: \d \w \s (10 min)

Common classes have a short form: a backslash and one letter. The uppercase version means the opposite.

ShorthandMeaningRoughly the same class
\done digit[0-9]
\Done character that isn't a digit[^0-9]
\wone letter, digit or underscore (Hangul included)wider than [a-zA-Z0-9_]
\Wone character that isn't \w
\sone whitespace character (space, tab, newline, etc.)wider than [ \t\n\r\f\v]
\Sone character that isn't whitespace
re.findall(r"\d", "B1 12가")       # ['1', '1', '2']
re.findall(r"\D", "12가-3")        # ['가', '-']
re.findall(r"\w", "a_1 가!")       # ['a', '_', '1', '가']
re.findall(r"\W", "a_1 가!")       # [' ', '!']
re.findall(r"\s", "a b\tc\nd")     # [' ', '\t', '\n']
re.findall(r"\S", "a b\tc")        # ['a', 'b', 'c']

You can mix shorthands with other characters inside brackets. [\d,.] is one character that is a digit, a comma or a period.

re.findall(r"\d[\d,.]\d", "1,2 3.4 5-6 789")   # ['1,2', '3.4', '789']

Now let's write the number plate with shorthands. Writing the middle as \D (not a digit) looks convenient, but then the camera's misreads 12-3457 and 12a3457 pass too. The middle shouldn't be "any non-digit"; it has to be "one Hangul syllable".

Evidence lab

re.search(r"", text)

Return value

\D happily accepts hyphens and letters too. Replace the middle \D with [가-힣] so only real plates remain.

2-5. Inside brackets, the rules change (5 min)

Inside brackets, most metacharacters lose their power. [.*+?] is just one character out of period, asterisk, plus and question mark. No backslashes needed.

Only four characters stay special inside brackets.

  • \ : escapes, and shorthands like \d
  • ] : the end of the class. But at the very front, it's a literal character
  • ^ : negation, when it's at the very front
  • - : a range. But at the very front or very end, it's a literal character
re.findall(r"[.*+?]", "a.b*c+d?")    # ['.', '*', '+', '?']
re.findall(r"[]a]", "a]b")           # ['a', ']']
re.findall(r"[-+]", "3-2+1")         # ['-', '+']
re.findall(r"[a\-z]", "a-mz")        # ['a', '-', 'z']
re.findall(r"[\]\\]", r"a]b\c")      # [']', '\\']

[a\-z] isn't a range; it's the three characters a, - and z. That's why m isn't caught. If the position rules get confusing, just adding a backslash is easier to read.

2-6. Unicode, and doubled symbols to avoid (5 min)

Python str patterns are Unicode by default, so the shorthands are wider than you might think. You've already seen \w catch Hangul. \d also counts the Arabic-Indic digit ٣ (U+0663, the digit 3) as a digit. \s catches the full-width space (U+3000) that often sneaks into Korean, Japanese and Chinese documents.

re.findall(r"\d", "3٣")          # ['3', '٣']
print(int("٣"))                  # 3
re.findall(r"\s", "a\u3000b")    # ['\u3000']
  • If you want exactly 0–9, use [0-9] instead of \d.
  • The re.ASCII flag, which narrows the shorthands to the ASCII range, comes in Lesson 11.

Finally: doubling a symbol inside brackets, as in [[, --, &&, ~~ or ||, raises a FutureWarning. Python is keeping those spots free for set operations between classes (union, intersection and so on) in the future.

import warnings
warnings.simplefilter("always")

re.compile(r"[[a]")      # FutureWarning: Possible nested set at position 1
re.compile(r"[a&&b]")    # FutureWarning: Possible set intersection at position 2

They work today, but their meaning may change later. If you really need those characters, escape one of them, as in [\[a] or [a&\&b]. We'll go over this again in Lesson 12.

Cracking the case (10 min)

Say the car park log is stored in a string called parking. First, pull out every real plate. The two lines the camera misread drop out.

plates = re.findall(r"\d\d[가-힣]\d\d\d\d", parking)
print(plates)   # ['12가3457', '34나1207', '17거3407', '56다9081']

Now turn the witness statement into classes, one position at a time. The first two digits are 1[27], the middle is [가거고], and the last is 7.

print(re.findall(r"1[27][가거고]\d\d\d7", parking))         # ['12가3457', '17거3407']
print(re.findall(r"OUT B\d 1[27][가거고]\d\d\d7", parking))  # ['OUT B2 17거3407']

There are two candidates, but 12가3457 came in before the accident and has no exit record. The only car that left through the exit, two minutes after the accident, is 17거3407.

Case closed report — 17거3407, which left through exit B2 at 22:47, matches every point of the witness statement. Asked Detective Kang to run a vehicle check.

Once Detective Kang has gone, the director taps the pattern on the screen. "\d\d[가-힣]\d\d\d\d. You wrote the same thing four times. And these days some plates have three digits up front." In the next lesson you'll learn how to shorten this long pattern.

Exercises

  1. Write a pattern that finds only the gate names, like B1 and B2, in the car park log. How do B[12] and B\d differ?
  2. What does [^\d\s] find? Make a guess on the camera log above, 12가3457 I2가3457 12가34S7 1Z거3407, then run it.
  3. Using shorthands, write a pattern that finds the two-digits:two-digits shape of a time like 22:41.

The director's notebook

  • [abc] is one character: a, b or c. However many you put in, it takes one position.

  • Ranges like [a-z0-9] follow code point order. Complete Hangul syllables are [가-힣]. [a-Z] is an error.

  • [^...] is negation only when ^ comes first. It catches newlines too.

  • \d \w \s and their opposites \D \W \S. Mix them inside a class, as in [\d,.].

  • Inside a class, . * + ? are literal characters. Only \ ] ^ - are special.

  • In str patterns, \d \w \s cover all of Unicode. For just 0-9, use [0-9].

← Previous

Lesson 1 · The missing payment: finding text as it is

Next →

Lesson 3 · The torn phone book: quantifiers

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