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 5

Time 3:20–4:10

Dates all over the place: groups and alternation

CASE-005

Grep & Co. Data Detective Agency

Case file

Dates all over the place

Client

Ms. Jeong, owner of the online store "Market Jeong"

Evidence

Market Jeong's order records. Each part-timer wrote the dates in a different shape.

To close the case

Pull the year, month and day out of every order separately and tidy them into one shape.

The day after the fake sign-up case, Ms. Jeong is back. This time she starts with a sigh. "We blocked the sign-ups. But when I tried to sort the order records by date, it turns out my three part-timers all wrote dates differently." The director takes a sip of coffee. "Part 1 taught you how to find things. Now it's time to learn how to pull things out of what you find."

Here are the order records.

#1001 2026-10-20 ship=done
#1002 2026.10.20 ship=wait
#1003 2026/10/19 ship=done
#1004 20261019 ship=done
#1005 2026-10/20 ship=wait

Just finding the dates isn't enough. To tidy them up you need "2026", "10" and "20" each on its own. Today's tool is parentheses: groups.

5-1. Wrapping in parentheses: quantifiers on groups (5 min)

In Lesson 3 you saw that a quantifier attaches only to the single piece right before it. Wrap several characters in parentheses ( ) and they become one piece.

re.fullmatch(r"ha+", "hahaha")      # None
re.fullmatch(r"(ha)+", "hahaha")    # <re.Match object; span=(0, 6), match='hahaha'>

A shape like an IP address, where "digits and a period" repeats three times, is also written with a quantifier on a group.

re.search(r"(\d{1,3}\.){3}\d{1,3}", "ip=203.0.113.66")
# <re.Match object; span=(3, 15), match='203.0.113.66'>

(\d{1,3}\.){3} is "1 to 3 digits and a period", three times. The last number has no period after it, so a separate \d{1,3} is tacked on.

5-2. Alternation: | this or that (8 min)

| means "the left side or the right side". But | has the lowest precedence of anything in a regex. Unless parentheses fence it in, it splits the whole pattern in two.

re.fullmatch(r"ship=done|wait", "wait")          # <re.Match object; span=(0, 4), match='wait'>
re.fullmatch(r"ship=(done|wait)", "wait")        # None
re.fullmatch(r"ship=(done|wait)", "ship=wait")   # <re.Match object; span=(0, 9), match='ship=wait'>

ship=done|wait is "ship=done or wait". If you want "ship= followed by done or wait", wrap only the part being chosen in parentheses.

Be especially careful when mixing it with anchors. ^a|b$ isn't "a string that is just a or just b"; it's "^a or b$".

re.findall(r"^a|b$", "a.b")         # ['a', 'b']
re.search(r"^ok|no$", "say no")     # <re.Match object; span=(4, 6), match='no'>

say no doesn't start with ok, yet it matched. For the intended meaning, write ^(ok|no)$.

The alternatives are tried from left to right, and the first one that succeeds wins. It doesn't pick the longest.

re.search(r"10|100", "100")   # <re.Match object; span=(0, 2), match='10'>
re.search(r"100|10", "100")   # <re.Match object; span=(0, 3), match='100'>
  • Put the longer alternatives first.
  • An alternation of single characters, -|\.|/, is the same as the class [-./]. In cases like this, the class is shorter and faster.

Evidence lab

re.search(r"", text)

Return value

ship=done|wait means 'ship=done' or 'wait'. Wrap only the part being chosen in parentheses.

5-3. Pulling things out by group number (8 min)

Parentheses have a second job besides grouping: they remember separately the text matched inside them. This is called a capturing group, and groups are numbered 1, 2, 3 … in the order their opening parentheses appear.

Say the order records above are stored in a string called orders.

m = re.search(r"(\d{4})-(\d\d)-(\d\d)", orders)
print(m.group(0))               # 2026-10-20
print(m.group(1))               # 2026
print(m.group(2), m.group(3))   # 10 20
print(m.groups())               # ('2026', '10', '20')
print(m.group(1, 2))            # ('2026', '10')
print(m[1], m[0])               # 2026 2026-10-20
  • group(0) (or just group()) is the whole match. Groups start at 1.
  • groups() gives you everything from group 1 to the last as a tuple.
  • Pass several numbers, as in group(1, 2), and you get a tuple back.
  • m[1] is short for m.group(1).

Nested parentheses follow the same rule: count in the order the opening parentheses ( appear.

((\d{4})-(\d\d))-(\d\d)
12       3       4
m = re.search(r"((\d{4})-(\d\d))-(\d\d)", "2026-10-20")
print(m.groups())     # ('2026-10', '2026', '10', '20')

Evidence lab

re.search(r"", text)

Return value

Wrap the year, month and day each in parentheses. Every group gets its own color, and m.group(1) to (3) appear in the panel below.

5-4. Parentheses that don't capture: (?:...) (5 min)

Often you only want to group, with no need to remember anything: when the parentheses are there to set the reach of |, or to hang a quantifier on. For that, use (?:...). It groups, but gets no number.

m = re.search(r"(?:\d{4})-(\d\d)", "2026-10")
print(m.group(1))                           # 10
print(re.fullmatch(r"(?:ab)+", "abab").groups())   # ()

The first pair of parentheses got no number, so (\d\d) became group 1. Capture only what you need and the numbering stays simple; you also dodge the findall trap in the next section.

5-5. findall and groups (8 min)

When the pattern has capturing groups, findall changes its behavior.

Number of capturing groupsWhat findall returns
0a list of whole-match strings
1a list of strings from that group
2 or morea list of tuples of the groups
re.findall(r"\d{4}[-./]\d\d[-./]\d\d", orders)
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']
re.findall(r"(\d{4})[-./]\d\d[-./]\d\d", orders)
# ['2026', '2026', '2026', '2026']
re.findall(r"(\d{4})[-./](\d\d)[-./](\d\d)", orders)
# [('2026', '10', '20'), ('2026', '10', '20'), ('2026', '10', '19'), ('2026', '10', '20')]

When you want the year, month and day, a list of tuples is handy. The problem is that parentheses you only meant for grouping become groups too.

re.findall(r"\d{4}(-|\.|/)\d\d(-|\.|/)\d\d", orders)
# [('-', '-'), ('.', '.'), ('/', '/'), ('-', '/')]

You wanted dates and got back only the separators. When you need the whole match, change those parentheses to (?:...), or use finditer to get Match objects and call group().

re.findall(r"\d{4}(?:-|\.|/)\d\d(?:-|\.|/)\d\d", orders)
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']
[m.group() for m in re.finditer(r"\d{4}(-|\.|/)\d\d(-|\.|/)\d\d", orders)]
# ['2026-10-20', '2026.10.20', '2026/10/19', '2026-10/20']

Dot's tip

If a findall result comes out strangely short, or turns into tuples, count the parentheses in your pattern first. Very often, parentheses you only meant for grouping have quietly become groups. That's also why running (ha)+ through findall in Lesson 3 gave you nothing but 'ha'.

Evidence lab

re.search(r"", text)

Return value

The findall result is tuples of separators. Change both parentheses to (?: , or pick finditer, so the whole date comes out.

5-6. Groups that didn't match, groups that repeated (6 min)

If a group doesn't take part in the match, its slot is None. That happens when a group with ? matched zero times, or when the other side of a | was chosen.

m = re.fullmatch(r"(\d{4})-(\d\d)(-(\d\d))?", "2026-10")
print(m.groups())     # ('2026', '10', None, None)
print(m.group(3))     # None
print(re.search(r"(a)|(b)", "b").groups())   # (None, 'b')

Put a quantifier on a group so it matches several times, and the group keeps only the last one.

m = re.fullmatch(r"(\d)+", "2026")
print(m.group(0), m.group(1))   # 2026 6

re.findall(r"(\d{1,3}\.){3}\d{1,3}", "a=203.0.113.66 b=198.51.100.7")
# ['113.', '100.']
re.findall(r"(?:\d{1,3}\.){3}\d{1,3}", "a=203.0.113.66 b=198.51.100.7")
# ['203.0.113.66', '198.51.100.7']

(\d)+ matched four times, but group 1 kept only the last 6. With the IP addresses, too, the group kept only the third repetition, 113., and findall returned that group. When you use the IP pattern from 5-1 with findall, (?:...) is the answer.

Cracking the case (10 min)

The three separators -, . and / are handled by [-./], and the separator-less 20261019 by ?. Capture the year, month and day separately.

The loop below prints the dates in Korean style: 년 is "year", 월 is "month", 일 is "day".

DATE = r"(\d{4})[-./]?(\d\d)[-./]?(\d\d)"

for m in re.finditer(DATE, orders):
    y, mo, d = m.groups()
    print(f"{y}년 {int(mo)}월 {int(d)}일")
# 2026년 10월 20일
# 2026년 10월 20일
# 2026년 10월 19일
# 2026년 10월 19일
# 2026년 10월 20일

To pull out the order number as well, add one more group. With four groups, findall returns four-item tuples.

re.findall(r"#(\d+) " + DATE, orders)
# [('1001', '2026', '10', '20'), ('1002', '2026', '10', '20'), ('1003', '2026', '10', '19'), ('1004', '2026', '10', '19'), ('1005', '2026', '10', '20')]

Evidence lab

re.search(r"", text)

Return value

This date pattern takes only hyphens. Change the separator spots to [-./]? so it takes all four shapes.

Case closed report — Split the dates of all 5 orders into year, month and day. However, #1005 was entered with mixed separators and needs checking by staff.

Ms. Jeong takes the tidy table and points at the last line. "#1005 says 2026-10/20. That one just passed too?"

re.fullmatch(DATE, "2026-10/20")
# <re.Match object; span=(0, 10), match='2026-10/20'>

She's right. The two [-./]? know nothing about each other. The current pattern has no way of saying "if you used - up front, use - at the back too". The director writes a line in her notebook. "There's a way to call back later what you caught earlier. That's Lesson 10." For today, a human eye checks this one entry, and that's that.

Exercises

  1. Predict the result of re.findall(r"ship=(done|wait)", orders). How does it change if you switch to (?:done|wait)?
  2. In re.search(r"(\d{4})-(\d\d)(-(\d\d))?", "2026-10 delivery"), what is groups()?
  3. Write a pattern that pulls the hours, minutes and seconds out of the time 03:11:02 as groups, and check the result of m.group(1, 3).

The director's notebook

  • ( ) wraps things into one piece and remembers the matched text. Add quantifiers, as in (ab)+.

  • | has the lowest precedence. ^a|b$ means '^a or b$'. Alternatives are tried from the left.

  • Group numbers follow the order of opening parentheses. group(0) is the whole match, groups() a tuple, m[1] is group(1).

  • (?:...) only groups; it gets no number.

  • findall: 0 groups gives whole matches, 1 gives that group, 2 or more give tuples. Need the whole match? Use (?:...) or finditer.

  • A group that didn't match is None; a repeated group like (\d)+ keeps only the last one.

← Previous

Lesson 4 · The fake sign-ups: anchors and validation

Next →

Lesson 6 · The 3 a.m. login: named groups and Match objects

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