Python regular expressions — exercises with validators, DNIs hidden in a report and two logs to merge (and the alert that said “All good”)

After the practice I got excited and wrote a tiny alert script: if the last line of the log had a 500 (server error) as a separate word, it should warn me. I used a word boundary so as not to confuse it with a 15000 in the bytes, just as I learnt in the theory. I tested it with a line that had a crystal-clear 500, and the script answered, very calmly: “All good”.

I went over the pattern ten times: \b500\b, perfect. The fault was right in front of it: I had written "\b500\b" **without the r**. Python turned each \b into a backspace character before the pattern reached re, and my script was looking for “backspace, 500, backspace”, something that appears in no log in the world. No error, no warning: just an alert that was never going to go off. It is the first snippet in “What’s wrong here?”, and the other two are just as silent.

Today we practise with three exercises of increasing difficulty, three snippets to debug and a cheat sheet to take away. These exercises use the regular expressions theory and review the practice. You can do them without those, but it will be harder.

How to use this article

  1. Read the statement of a level and don’t look at the solution yet.
  2. Write your version, run it and compare your output with the expected one.
  3. If you get stuck, look only at the 🔵 hint and try again.
  4. When you finish, compare with the solution and pay attention to why it is written that way, not just to whether it gives the same output.

No exercise uses input(): the data is fixed in the code. With regular expressions, matching output is not enough: also test your pattern with texts that should not match. A pattern that accepts everything also “gives the expected output” with the good examples.

Level 1 · Basic: validating the fields of an alert

Statement. Before an alert is created, the system checks that each field has the right format. Write a dictionary PATTERNS with one pattern per field:

  • IP: four numbers of 1 to 3 digits separated by dots.
  • Time: hh:mm:ss with hours from 00 to 23 and minutes and seconds from 00 to 59.
  • Code: an HTTP code, from 100 to 599.
  • Path: starts with / and goes on with letters, digits, _, /, . or -.

Check each value from these tests with re.fullmatch and, for comparison, also with re.match, and show a table with ✔ or ✘:

TESTS = [
    ("IP", "10.0.0.7"), ("IP", "10.0.0"), ("IP", "10.0.0.7 "),
    ("Time", "09:15:02"), ("Time", "9:15:02"), ("Time", "24:00:00"),
    ("Code", "404"), ("Code", "4040"), ("Code", "704"),
    ("Path", "/fp1/tema8.pdf"), ("Path", "fp1/tema8.pdf"),
]

Expected output:

Field   Value             fullmatch  match
IP      '10.0.0.7'        ✔          ✔
IP      '10.0.0'          ✘          ✘
IP      '10.0.0.7 '       ✘          ✔
Time    '09:15:02'        ✔          ✔
Time    '9:15:02'         ✘          ✘
Time    '24:00:00'        ✘          ✘
Code    '404'             ✔          ✔
Code    '4040'            ✘          ✔
Code    '704'             ✘          ✘
Path    '/fp1/tema8.pdf'  ✔          ✔
Path    'fp1/tema8.pdf'   ✘          ✘

For the hours, \d\d is not enough: think of an alternative between “a 0 or a 1 followed by any digit” and “a 2 followed by a digit from 0 to 3”. For the code, the first digit is a set from 1 to 5. And for the IP, repeat a non-capturing group (?:…) three times.

Level 2 · Intermediate: DNIs hidden in an incident report

Statement. Each technician writes the virtual campus incident report their own way, and the DNIs (the Spanish national ID: eight digits and a check letter) appear stuck to the letter, separated by a hyphen or a space, and with a lowercase letter:

INCIDENTS = """\
09:15 user 12345678Z cannot remember their password
09:31 locked 87654321-x after three attempts
10:02 user 45123456 D asks to reopen the submission
10:40 contact phone: 928451000 (not a DNI)
11:05 new accounts 11111111A and 2222222-J
"""

Find every DNI (eight digits and a letter, with a hyphen, a space or nothing in between) with a single pattern. For each one, show the text as it appears, the normalised DNI (digits and capital letter, all together) and whether the letter is correct. The correct letter is LETTERS[number % 23], with LETTERS = "TRWAGMYFPDXBNJZSQVHLCKE", as in the strings topic. Finish with a summary.

The phone number (nine digits) and 2222222-J (seven digits) are not DNIs and must not appear.

Expected output:

Found         Normalized   Letter
'12345678Z'   12345678Z    correct
'87654321-x'  87654321X    correct
'45123456 D'  45123456D    correct
'11111111A'   11111111A    should be H
DNIs: 4, with the correct letter: 3

Two groups, one for the digits and one for the letter, with [- ]? between them. To avoid finding eight digits inside the phone number, surround the pattern with word boundaries \b. And since there are several DNIs and you want their groups, go through the matches with re.finditer.

Level 3 · Challenge: merging two logs with different dates

Statement. Two servers write their logs with different date formats: A uses day-month-year (with / or with -) and B, year-month-day. On top of that, some line was typed by hand and has mixed separators:

SERVER_A = """\
12/10/2026 09:15:02 10.0.0.7 GET /campus/inicio 200
12-10-2026 09:31:08 192.168.1.66 POST /campus/login 401
12/10-2026 09:40:00 10.0.0.9 GET /fp1 200
"""
SERVER_B = """\
2026-10-12 09:20:17 172.16.4.150 GET /fp1/tema8.pdf 200
2026-10-12 10:02:33 10.0.0.41 GET /fp1/entregas 500
2026/10-12 10:05:12 10.0.0.41 GET /fp1/entregas 200
"""

Write a function normalize(line) that returns the line with the date as year-month-day with hyphens, or None if the date is not valid. A day-month-year date is only valid if it uses the same separator twice. Put together the valid lines from both servers, marking their source, sort them chronologically and show them. Then list the rejected ones and count how many requests have an error code (4xx or 5xx).

Expected output:

2026-10-12 09:15:02 10.0.0.7 GET /campus/inicio 200  [A]
2026-10-12 09:20:17 172.16.4.150 GET /fp1/tema8.pdf 200  [B]
2026-10-12 09:31:08 192.168.1.66 POST /campus/login 401  [A]
2026-10-12 10:02:33 10.0.0.41 GET /fp1/entregas 500  [B]
Rejected:
  A: 12/10-2026 09:40:00 10.0.0.9 GET /fp1 200
  B: 2026/10-12 10:05:12 10.0.0.41 GET /fp1/entregas 200
Requests: 4, with errors: 2

For “the same separator”, capture the first one in a group and then require a backreference to that group. re.subn with a template reorders the date and, at the same time, tells you whether it matched. Once every date is year-month-day, sort() on the lines leaves them in chronological order.

What’s wrong here?

Three snippets with one bug each. Before reading the explanation, do two things: predict whether it raises an error and, if it doesn’t, what it prints.

Snippet 1

raw.py

import re

line = '10.0.0.41 [12/10/2026 10:02:33] "GET /fp1/entregas" 500 87 "Chrome/129"'
if re.search("\b500\b", line):        # is there a 500 as a separate word?
    print("Server error")
else:
    print("All good")

*Think before reading on: the line has a 500 as a separate word. What does python3 raw.py print?*

Explanation. No error, but it gets it wrong:

All good

It is my alert from the start. The pattern "\b500\b" is written **without r**, and Python processes the string’s escape sequences before handing it to re. \b is a valid sequence for Python: the backspace character (code 8). So re does not receive “boundary, 500, boundary” but “backspace, 500, backspace”. There is no backspace in the line, re.search returns None and the else prints “All good”.

Trusting that the pattern “works without r” because it worked with \d. \d is not a Python escape and reaches re intact; \b, \1 or \n are, and they are transformed on the way.

Fix. An r in front of the pattern:

import re

line = '10.0.0.41 [12/10/2026 10:02:33] "GET /fp1/entregas" 500 87 "Chrome/129"'
if re.search(r"\b500\b", line):       # with r: \b reaches re as a word boundary
    print("Server error")
else:
    print("All good")

When you run it:

Server error

If you ever wonder what re actually received, print repr(pattern). repr("\b500\b") shows '\x08500\x08': the backspaces are out in the open.

Snippet 2

avaricia.py

import re

line = '10.0.0.41 [12/10/2026 10:02:33] "GET /fp1/entregas" 500 87 "Chrome/129"'
request = re.search(r'"(.*)"', line).group(1)   # whatever is between quotes
print(request)

*Think before reading on: we want the request between quotes, GET /fp1/entregas. What does it print?*

Explanation. No error, but it grabs half the line:

GET /fp1/entregas" 500 87 "Chrome/129

.* is greedy: after the first quote, it swallows the rest of the line and then steps back just enough for the pattern’s quote to match, which is the last one in the line, the one closing "Chrome/129". Everything in between, two quotes included, goes into group 1.

Testing the pattern with a line that has only one pair of quotes. There "(.*)" works, and the bug shows up with the first real line that has two.

Fix. Describe what can go inside: anything except a quote.

import re

line = '10.0.0.41 [12/10/2026 10:02:33] "GET /fp1/entregas" 500 87 "Chrome/129"'
request = re.search(r'"([^"]*)"', line).group(1)   # nothing that is a quote
print(request)

When you run it:

GET /fp1/entregas

"(.*?)", with the lazy quantifier, fixes it too. [^"]* has one advantage: whatever the rest of the pattern becomes, it can never skip over a quote, whereas the lazy one can end up stretching if what follows does not match the first time.

Snippet 3

match.py

import re

lines = [
    '10.0.0.41 [12/10/2026 10:02:33] "GET /fp1/entregas" 500 87 "Chrome/129"',
    '192.168.1.66 [12/10/2026 09:31:08] "POST /campus/login" 401 312 "curl/8.5"',
]
for line in lines:
    m = re.match(r"\d{3}", line)      # the HTTP code is 3 digits
    print(m.group() if m else "no code")

Think before reading on: we want the HTTP code of each line (500 and 401). What does it print?

Explanation. No error, and both answers are wrong, each in its own way:

no code
192

re.match only tries at the start of the line. The first line starts with 10.0.0.41: after the 1 and the 0 comes a dot, so there are no three digits in a row at position 0, and match returns None without looking any further. The second starts with 192.168.1.66, and 192 are three digits: match matches the first number of the IP and the program prints 192 as if it were the code.

The second bug is the worst kind: a result that looks right. Even if match is swapped for search, \d{3} would find the first run of three digits in the line, which is not the code either.

Looking for a field only by its shape (\d{3}) when other fields have the same shape. In a log, IPs, dates, bytes and codes are all digits: what sets the code apart is where it is.

Fix. search to look through the whole line, and context around the three digits so that only the code matches: the closing quote and the spaces.

import re

lines = [
    '10.0.0.41 [12/10/2026 10:02:33] "GET /fp1/entregas" 500 87 "Chrome/129"',
    '192.168.1.66 [12/10/2026 09:31:08] "POST /campus/login" 401 312 "curl/8.5"',
]
for line in lines:
    m = re.search(r'" (\d{3}) ', line)   # search looks in the whole line; the context pins it down
    print(m.group(1) if m else "no code")

When you run it:

500
401

Solutions

Look at this section only once you have tried the exercise. If your code is different but gives the same output (and rejects the same things), it is also a valid solution.

Level 1 solution

validar.py

import re

PATTERNS = {
    "IP": r"\d{1,3}(?:\.\d{1,3}){3}",
    "Time": r"([01]\d|2[0-3]):[0-5]\d:[0-5]\d",   # 00 to 23 : 00 to 59 : 00 to 59
    "Code": r"[1-5]\d\d",                         # HTTP codes go from 100 to 599
    "Path": r"/[\w/.-]*",                         # starts with / and then letters, /, . or -
}
TESTS = [
    ("IP", "10.0.0.7"), ("IP", "10.0.0"), ("IP", "10.0.0.7 "),
    ("Time", "09:15:02"), ("Time", "9:15:02"), ("Time", "24:00:00"),
    ("Code", "404"), ("Code", "4040"), ("Code", "704"),
    ("Path", "/fp1/tema8.pdf"), ("Path", "fp1/tema8.pdf"),
]

print(f"{'Field':<8}{'Value':<18}{'fullmatch':<11}match")
for field, value in TESTS:
    good = re.fullmatch(PATTERNS[field], value)   # does the WHOLE value have the format?
    almost = re.match(PATTERNS[field], value)     # does it START with the format?
    print(f"{field:<8}{value!r:<18}{'✔' if good else '✘':<11}{'✔' if almost else '✘'}")

Why it is written this way:

  • The patterns are stored in a dictionary by field, so the check is a single line for all of them: re.fullmatch(PATTERNS[field], value). Adding a new field means adding a key.
  • The time uses ([01]\d|2[0-3]): “a 0 or a 1 and any digit” (00 to 19) or “a 2 and a digit from 0 to 3” (20 to 23). Minutes and seconds, [0-5]\d, go from 00 to 59. That way 24:00:00 is rejected by its format, without converting anything to a number.
  • The code is [1-5]\d\d: the first digit from 1 to 5. That is why 704 fails, even though it has three digits.
  • In the path, [\w/.-]* accepts letters, digits, _, slashes, dots and hyphens. The hyphen goes at the end of the set: placed between two characters it would mean a range. And inside the brackets the dot is an ordinary dot, unescaped.
  • The match column shows why validating with match is a mistake: it accepts '10.0.0.7 ' (with a space after it) and '4040', because it only checks that the value starts well. fullmatch requires nothing to be left over.
  • '10.0.0' and '9:15:02' fail with both functions: a piece is missing (a number of the IP, a digit of the hour) from the start.
  • {value!r:<18} prints the value with repr, in quotes. That way you can see the trailing space of '10.0.0.7 ', which would be invisible with an ordinary print.

\d{1,3} accepts 999.999.999.999, which is not a valid IP (each number goes from 0 to 255). Regexes check formats; for ranges of values, the clearest thing is to convert with int() and compare. Mixing both in one pattern is possible, but the result is unreadable.

Level 2 solution

dnis.py

import re

LETTERS = "TRWAGMYFPDXBNJZSQVHLCKE"     # the DNI letter is LETTERS[number % 23] (topic 7)

INCIDENTS = """\
09:15 user 12345678Z cannot remember their password
09:31 locked 87654321-x after three attempts
10:02 user 45123456 D asks to reopen the submission
10:40 contact phone: 928451000 (not a DNI)
11:05 new accounts 11111111A and 2222222-J
"""
# 8 digits (group 1), an optional hyphen or space, and the letter (group 2), with boundaries on both sides
DNI = r"\b(\d{8})[- ]?([A-Za-z])\b"

total = correct = 0
print(f"{'Found':<14}{'Normalized':<13}Letter")
for m in re.finditer(DNI, INCIDENTS):
    number, letter = m.group(1), m.group(2).upper()
    expected = LETTERS[int(number) % 23]
    total += 1
    if letter == expected:
        correct += 1
        status = "correct"
    else:
        status = f"should be {expected}"
    print(f"{m.group()!r:<14}{number + letter:<13}{status}")
print(f"DNIs: {total}, with the correct letter: {correct}")
The DNI pattern against five candidates (n2)The pattern \b(\d{8})[- ]?([A-Za-z])\b in pieces and five texts from the report. 12345678Z, 87654321-x and 45123456 D match: eight digits, an optional separator and a letter. 928451000 does not, because after eight digits comes a 0, and neither does 2222222-J, which has only seven digits.The DNI pattern against five candidates (n2)\b(\d{8})[-␣]?([A-Za-z])\bboundarygroup 1: 8 digitshyphen, space or nothinggroup 2: the letterboundary12345678Z✔no separator87654321–x✔with hyphen; upper() → X45123456␣D✔with a space928451000✘8 digits and a 0: not a letter2222222–J✘only 7 digits: \d{8} fails
[- ]? makes the separator optional; the two \b stop the pattern from matching inside a longer number.

Why it is written this way:

  • The pattern \b(\d{8})[- ]?([A-Za-z])\b has two groups: the eight digits and the letter. What lies between them ([- ]?) is “a hyphen or a space, or nothing”, and it is not captured because we don’t want it in the normalised DNI.
  • The boundaries \b do the fine work. In 928451000, the first eight digits match \d{8}, but the next thing is a 0, which is neither a separator nor a letter. The engine tries starting at the next digit, but there is no boundary there (it is in the middle of the number), and the same goes for all the others: the phone number does not match at any position.
  • 2222222-J has only seven digits, so \d{8} does not match. Without the first \b it would not match either, but the final \b is the one that prevents, for example, 12345678ZZ from being taken as a DNI with the letter Z.
  • **re.finditer** gives one Match per DNI, with its groups. With findall you would get (digits, letter) tuples, but not the complete original text (m.group()), which is what the first column shows.
  • The letter is uppercased with upper() before comparing. 87654321-x is a correct DNI written with a lowercase letter.
  • 11111111A is found, because it has the format of a DNI, but its letter is wrong: 11111111 % 23 = 18, and LETTERS[18] is H. Once again, the regex checks the shape and the calculation checks the value.
  • The summary comes from two counters: 4 DNIs found, of which 3 have the correct letter.

total = correct = 0 sets both variables to 0 in one line. It works fine with numbers; with lists, on the other hand, both variables would point to the same list.

Level 3 solution

unificar.py

import re

# Two servers, two date formats (and the odd line typed by hand, wrongly)
SERVER_A = """\
12/10/2026 09:15:02 10.0.0.7 GET /campus/inicio 200
12-10-2026 09:31:08 192.168.1.66 POST /campus/login 401
12/10-2026 09:40:00 10.0.0.9 GET /fp1 200
"""
SERVER_B = """\
2026-10-12 09:20:17 172.16.4.150 GET /fp1/tema8.pdf 200
2026-10-12 10:02:33 10.0.0.41 GET /fp1/entregas 500
2026/10-12 10:05:12 10.0.0.41 GET /fp1/entregas 200
"""
DMY = r"(\d\d)([-/])(\d\d)\2(\d{4}) "   # day, separator, month, THE SAME separator, year
YMD = r"\d{4}-\d\d-\d\d "               # already year-month-day


def normalize(line):
    """Returns the line with the date as year-month-day, or None if the date makes no sense."""
    new, n = re.subn("^" + DMY, r"\4-\3-\1 ", line)
    if n == 1:
        return new
    if re.match(YMD, line):
        return line
    return None


merged, rejected = [], []
for source, text in [("A", SERVER_A), ("B", SERVER_B)]:
    for line in text.splitlines():
        new = normalize(line)
        if new is None:
            rejected.append(f"{source}: {line}")
        else:
            merged.append(new + "  [" + source + "]")

merged.sort()          # with the date as year-month-day, alphabetical order is chronological order
for line in merged:
    print(line)

print("Rejected:")
for r in rejected:
    print("  " + r)

errors = [l for l in merged if re.search(r" [45]\d\d  \[", l)]   # 4xx and 5xx codes
print(f"Requests: {len(merged)}, with errors: {len(errors)}")
\2 requires the same separator (n3)The pattern (\d\d)([-/])(\d\d)\2(\d{4}) against two dates. In 12-10-2026 group 2 captures a hyphen and position 5 holds a hyphen again: it matches, and the template \4-\3-\1 turns it into 2026-10-12. In 12/10-2026 group 2 captures a slash but position 5 holds a hyphen: no match.\2 requires the same separator (n3)(\d\d)([-/])(\d\d)\2(\d{4})1234the same text12–10–2026captures '-'✔ equal: matches→ 2026-10-1212/10–2026captures '/'✘ different: no match
A group captures one specific text; \2 does not repeat the pattern [-/], it repeats THAT text. That is why the mixed date is rejected.

Why it is written this way:

  • DMY is (\d\d)([-/])(\d\d)\2(\d{4}) : group 2 captures the first separator, and \2 requires the same text in the second slot. 12-10-2026 matches (hyphen and hyphen); 12/10-2026 does not (slash and hyphen). It is exactly what the statement asked for, in a single pattern.
  • normalize uses re.subn("^" + DMY, r"\4-\3-\1 ", line). The ^ makes sure the date is at the start of the line. The template writes the year (group 4), the month (group 3) and the day (group 1); the separator (group 2) is not used, because the output always has hyphens. The trailing space is repeated in the template because it was part of what matched.
  • If n is 1, the date was day-month-year and it has already been converted. If not, the function checks with re.match(YMD, line) whether the line was already year-month-day; if not that either, it returns None. That is how 2026/10-12 is rejected: it has neither of the two formats.
  • "^" + DMY joins two strings: the "^" has no r because it has no backslashes, and DMY was already written with r. What reaches re is the complete, well-formed pattern.
  • The valid lines are stored with their source at the end (" [A]"), and **merged.sort()** sorts them as text. Since they all start with 2026-10-12 hh:mm:ss, alphabetical order matches chronological order: the lines from A and B end up interleaved by time.
  • The last pattern, r" [45]\d\d \[", counts the errors: a space, a code starting with 4 or 5, two spaces and the bracket of the source. The context avoids confusing the code with other numbers in the line. There are 2: the 401 from server A and the 500 from B.

Watch it step by step in Python Tutor

The bug in snippet 2 is much easier to understand by comparing both versions side by side. Python Tutor runs the code line by line and shows the value of each variable. Choose Python 3 and paste this program:

import re

line = '"GET /fp1" 200 "Chrome/129"'
greedy = re.search(r'"(.*)"', line)
lazy = re.search(r'"(.*?)"', line)
print(greedy.group(1))
print(lazy.group(1))

Expected output:

GET /fp1" 200 "Chrome/129
GET /fp1

What to watch, step by step:

  1. After line 3, line is a string with four quotes: two around the request and two around the browser.
  2. Lines 4 and 5 create two re.Match objects, greedy and lazy, with the same text and almost the same pattern: only a ? changes.
  3. On line 6, greedy.group(1) starts after the first quote and ends before the last one: the group contains 200 and the two quotes in the middle.
  4. On line 7, lazy.group(1) ends at the second quote, the first one that lets the pattern close: just GET /fp1.
  5. Change the pattern of lazy to r'"([^"]*)"' and run it again: the result is the same as with .*?.

Cheat sheet: Python regular expressions

The essentials in 30 seconds

  • **import re and every pattern with r in front:** r"\b\d{3}\b".
  • To search: re.search (anywhere). To validate: re.fullmatch (everything). At the start: re.match.
  • Every match: re.findall (list of strings) or re.finditer (one Match each).
  • **Check for None** before asking for m.group().
  • **Groups ( )** to cut apart: m.group(1), m.groups(). They are numbered by the opening parenthesis.
  • **.* is greedy:** for “between quotes”, "[^"]*" or ".*?".

Pattern syntax

I want…PatternExample that matches
A digit / non-digit\d / \D7 / a
Word character / not\w / \Wa, 7, _ / .
Whitespace / not\s / \Sspace, tab / x
Any character (except \n).a, 7, .
A real dot\..
One of several characters[aeiou], [0-9a-f]e, 3
Anything except those[^0-9]x
One thing or anotherGET|POSTPOST
Optionalhttps?http, https
0 or more / 1 or more\d* / \d+"" / 404
Exactly n / between n and m\d{4} / \d{1,3}2026 / 16
The minimum (lazy).*?, \d+?
Start / end of the text^ / $
Separate word\b404\b404, not 14040
Capturing group(\d\d)
Non-capturing group(?:\.\d+){3}.16.4.150
The same as group 1\1

Functions of the re module

I want…CodeReturns
Is it anywhere?re.search(p, t)Match or None
Does it start like this?re.match(p, t)Match or None
Is it all like this?re.fullmatch(p, t)Match or None
Every matchre.findall(p, t)List of strings (or of groups)
Go through them with positionsfor m in re.finditer(p, t):One Match per loop
Replacere.sub(p, r"\2/\1", t)New string
Replace and countre.subn(p, rep, t)(new string, n)
Check a patternre.compile(p)Pattern or re.error

The Match object

I want…CodeExample
The whole matchm.group() or m.group(0)'[12/10/2026 09:15:40]'
One groupm.group(1)'12'
Several groupsm.group(1, 2, 3)('12', '10', '2026')
All the groupsm.groups()Tuple; None for the ones that don’t take part
Positionm.start(), m.end(), m.span()(52, 55)
Position of a groupm.span(4)(22, 30)

Templates from the exercises

Validate a value (Level 1):

if re.fullmatch(r"([01]\d|2[0-3]):[0-5]\d:[0-5]\d", time):
    print("Valid time")

Search while checking for None (snippet 3):

m = re.search(r'" (\d{3}) ', line)
if m:
    code = int(m.group(1))

Go through every match with its groups (Level 2):

for m in re.finditer(r"\b(\d{8})[- ]?([A-Za-z])\b", text):
    number, letter = m.group(1), m.group(2).upper()

Cut a line into fields (practice):

m = re.fullmatch(PATTERN, line)
if m is None:
    continue
ip, date, time = m.groups()

Reorder with a template (Level 3):

new, n = re.subn(r"^(\d\d)([-/])(\d\d)\2(\d{4})", r"\4-\3-\1", line)
if n == 0:
    print("It did not have the format")

Most frequent errors

SymptomLikely cause
AttributeError: 'NoneType' object has no attribute 'group'The pattern found nothing and you didn’t check if m:
A pattern with \b or \1 finds nothing, with no errorThe r in front is missing: Python turned the backslash into an escape
14040 slips through when looking for 404Boundaries are missing: \b404\b
".*" grabs half the lineGreedy: use "[^"]*" or ".*?"
re.match gives None for something that is therematch only looks at the start: use search
A validator accepts '4040' or '10.0.0.7 'You validate with match or search: use fullmatch
10a0b0c7 passes as an IPUnescaped dot: \.
findall returns tuples or just one pieceThe pattern has groups: findall returns the groups
A repeated group keeps only the last piece(…){3} keeps the last round: use several groups or finditer
Group numbers “move”You added parentheses: use (?:…) for the ones that only group
re.error: missing ), unterminated subpatternUnclosed parenthesis in the pattern
re.sub “doesn’t change” the textIt returns a new one: t = re.sub(…, t)

Before handing in, ask yourself

  1. Do all my patterns and templates carry the r in front?
  2. Do I use fullmatch to validate and search to look for things, and do I check that the result is not None?
  3. Have I escaped the dots, brackets and parentheses I want to find as they are?
  4. Have I also tested each pattern with texts that should not match?

If you want to review the topic from the beginning, go back to the regular expressions theory.

Keep goingAll Python (en) articles

Did this article help you? Here is how you can support SergioLearns →

Sigue aprendiendo

Leave a Reply

Your email address will not be published. Required fields are marked *