Skip to content
wannacode

Search and replace

Problem

The bank statement

Your bank sends you text with the payments for the month. You want each amount separately, so you can add them up:

import re
statement = "Rent €450.00 on 01.10, power €87.35 on 05.10, water €23.10 on 12.10"
print(re.fullmatch(r".*€\d+\.\d{2}.*", statement) is not None)
Output
True
fullmatch only says "yes, there is an amount somewhere". The .* on both sides is there because fullmatch wants the pattern to cover the whole text. re.sub would change the amounts, but it does not give them to you. You need a way to find each match and take it.

The idea

re.compile and finditer

For searching, the re module has a pattern object and a match object:

import re
statement = "Rent €450.00 on 01.10, power €87.35 on 05.10, water €23.10 on 12.10"
amount = re.compile(r"€\d+\.\d{2}")
for m in amount.finditer(statement):
print(m.group())
Output
€450.00
€87.35
€23.10
  • re.compile(...) turns the pattern into a Pattern object. Do it once and use it as many times as you like.
  • amount.finditer(statement) gives the matches one by one, from left to right. That is why it is in a for: each iteration gets the next one, until they run out.
  • m is a Match object: one match that was found. m.group() is its text.
More precisely, if you are curious
  • re.findall gives you a list with the texts of the matches right away, without the Match objects.
  • re.search gives only the first match, wherever it is, or None. re.match checks only whether the text starts with a match: for "Table 12" it gives None, even though 12 is in the text.
  • re.sub, re.split and re.fullmatch compile the pattern themselves and keep the last few patterns ready. re.compile is handy when one pattern is used in many places.
import re
statement = "Rent €450.00 on 01.10, power €87.35 on 05.10, water €23.10 on 12.10"
print(re.findall(r"€\d+\.\d{2}", statement))
print(re.match(r"\d+", "Table 12"))
print(re.match(r"\d+", "12 tables").group())
Output
['€450.00', '€87.35', '€23.10']
None
12

See it

Where the match is

Besides the text, the match also knows where it was found:

import re
log = "ERROR disk; INFO start; ERROR network"
count = 0
for m in re.finditer("ERROR", log):
count += 1
print(f"{m.group()} from {m.start()} to {m.end()}")
print(f"Total: {count}")
Output
ERROR from 0 to 5
ERROR from 24 to 29
Total: 2
m.start() is the index of the first character of the match, and m.end() is the index after the last one, as with a slice. That is why log[m.start():m.end()] is the same as m.group().

Try it

A digit or a number?

What will the program print?

import re
for m in re.finditer(r"\d+", "Table 12, 4 people, 2 bottles"):
print(f"[{m.group()}]", end="")
print()
Answer

The idea

Groups: parts of the match

Parentheses in the pattern make a group. The match remembers what matched each group separately:

import re
date = re.compile(r"(\d{2})\.(\d{2})\.(\d{4})")
m = date.search("Due: 15.11.2026")
if m:
print(m.group())
print(m.group(1) + " / " + m.group(2) + " / " + m.group(3))
print(m.group(3) + "-" + m.group(2) + "-" + m.group(1))
Output
15.11.2026
15 / 11 / 2026
2026-11-15
  • Groups are numbered by their opening parentheses, from left to right, starting at 1.
  • m.group(1) is group 1. group(0) or group() is the whole match.
  • search is for when you need only the first match. If there is none, m is None and if m: skips it.

This way you take the day, the month and the year of a date separately, and arrange them the way you need.

import re
date = re.compile(r"(\d{2})\.(\d{2})\.(\d{4})")
m = date.search("Due: 15.11.2026")
if m:
print(m.group())
print(m.group(1) + " / " + m.group(2) + " / " + m.group(3))
print(m.group(3) + "-" + m.group(2) + "-" + m.group(1))
Step by step, in words
  1. The date pattern has three pairs of parentheses: three groups.
  2. search looks for the pattern in the text. If it finds it, m is the match (Match), otherwise None.
  3. search finds the date: from index 5 to 15.
  4. Each group remembers its part: group 1 is 15, group 2 is 11, group 3 is 2026.
  5. The parts sit in m, each under its own number. group(0) is the whole match.
  6. group() is the same as group(0): 15.11.2026.
  7. The three groups, one by one.
  8. In reverse order, the parts give 2026-11-15.
More precisely, if you are curious
  • (?:...) is a group that only holds the parts together, without remembering them. For example, (?:\+359|0)8[789]\d{7} does not make a group 1.
  • date.groups gives the number of groups in the pattern.
  • If a group did not take part in the match (for example, (\.\d{2})? was not there), the group is None.
import re
date = re.compile(r"(\d{2})\.(\d{2})\.(\d{4})")
print(date.groups)
m = re.search(r"€(\d+)(\.\d{2})?", "Tip €5")
print(m.group(1), m.group(2))
Output
3
5 None

The idea

Named groups and replacing

When there are many groups, the numbers get mixed up. You can name them: (?P<name>...), and then m.group("name"). In Python there is a P after the question mark: the form (?<name>...) from Java and JavaScript is an error in Python 3.12. Here is a line from a café bill:
import re
item = re.compile(r"(?P<qty>\d+) x (?P<name>[A-Za-z ]+) at (?P<price>\d+\.\d{2})")
for m in item.finditer("2 x Coffee with milk at 3.20; 1 x Croissant at 2.90"):
qty = int(m.group("qty"))
price = float(m.group("price"))
print(f"{m.group('name')}: €{qty * price}")
Output
Coffee with milk: €6.4
Croissant: €2.9
Groups work in re.sub too: in the replacement text, \1 is group 1, and \g<name> is the group with that name. The replacement text is a raw string too, because of the \. Here are the dates, from the Bulgarian form to ISO and to American:
import re
text = "Deadline: 15.11.2026, reminder: 08.11.2026"
print(re.sub(r"(\d{2})\.(\d{2})\.(\d{4})", r"\3-\2-\1", text))
print(re.sub(r"(?P<d>\d{2})\.(?P<m>\d{2})\.(?P<y>\d{4})", r"\g<m>/\g<d>/\g<y>", text))
Output
Deadline: 2026-11-15, reminder: 2026-11-08
Deadline: 11/15/2026, reminder: 11/08/2026
Here you do not need re.compile and finditer: re.sub understands groups by itself.
More precisely, if you are curious
  • \ in the replacement text is special. If the text comes from the user, for example a path to a folder, re.sub may stop with an error. Double the backslashes in it with replace("\\", "\\\\"):
import re
user = "C:\\Temp"
try:
re.sub("FOLDER", user, "Copy in FOLDER")
except re.error as e:
print(e)
print(re.sub("FOLDER", user.replace("\\", "\\\\"), "Copy in FOLDER"))
Output
bad escape \T at position 2
Copy in C:\Temp
  • You can also use a group in the pattern itself: \1 means "the same as group 1". This is how you catch repeated words. See the card on flags.

Try it

How much does + take?

You want all the HTML tags from the line:

What will the program print?

import re
html = "<b>Price:</b> <i>€12</i>"
for m in re.finditer(r"<.+>", html):
print(m.group())
Answer

The idea

Greedy and lazy

A ? after a quantifier makes it lazy: it takes as little as it can and stops at the first chance.

import re
html = "<b>Price:</b> <i>€12</i>"
for m in re.finditer(r"<.+?>", html):
print(m.group())
print(re.sub(r"<[^>]+>", "", html))
Output
<b>
</b>
<i>
</i>
Price: €12
  • <.+?> is "<, at least one character, and the first > after it".
  • <[^>]+> does the same in another way: "<, characters that are not >, and >". It cannot skip past a >, so it is also faster. With it you remove the tags, and only the text is left.
Greedy Lazy
* *?
+ +?
? ??
{2,5} {2,5}?

Try it

Find the bug

The program should print the three order numbers, but it prints only #1042. Which line has the bug?

Pick the line with the bug.

The idea

Flags and lines

You give the flags as the second argument of re.compile, joined with |. From a log file with several lines, you take the error messages, written in upper or lower case:

import re
log = "INFO: Start\nERROR: No connection to the database\nINFO: Retrying\nerror: Disk 95%"
p = re.compile(r"^error: (.+)$", re.IGNORECASE | re.MULTILINE)
for m in p.finditer(log):
print(m.group(1))
Output
No connection to the database
Disk 95%
  • re.IGNORECASE is the same as (?i): upper and lower case letters are the same.
  • re.MULTILINE makes ^ and $ the start and end of each line. Without it, they are only the start and end of the whole text, and the pattern will find nothing.
  • re.DOTALL lets . catch a new line too.
More precisely, if you are curious

Two things you will run into in other people's patterns:

  • A backreference \1: "the same text as group 1". This is how you catch repeated words.
  • A lookahead (?=...): "somewhere from here on there is ...", without taking any characters. Several of them in a row are handy for passwords: "has a digit" and "has a capital letter", and at least 8 characters.
import re
twice = re.compile(r"\b(\w+) \1\b")
for m in twice.finditer("Meet me at the café café, please please be on time."):
print("Repeated: " + m.group(1))
strong = r"(?=.*\d)(?=.*[A-Z]).{8,}"
print(re.fullmatch(strong, "secret12") is not None, re.fullmatch(strong, "Secret12") is not None)
Output
Repeated: café
Repeated: please
False True

(?!...) is the opposite: "from here on there is no ...". There is also lookbehind: (?<=...) and (?<!...).

Exercise

Find, extract, replace

Solve the tasks in your editor. The text is one line from the keyboard. In the lesson "Files" you will do them with a file too.

1. Find and replace. Replace the word "old" with "new" everywhere it is a whole word, and say how many replacements you made. Parts of other words ("oldest") are left alone. re.subn does the same as re.sub, but returns a pair: the new text and the number of replacements.

Input
This is old content. More old content, not the oldest.
Output
This is new content. More new content, not the oldest.
Replaced: 2

2. Words with a prefix. The first line is the prefix, the second is the text. Print each word that starts with the prefix.

Input
app
apple application banana appreciate snapple
Output
apple
application
appreciate

3. Addresses in text. Extract all web addresses that start with http:// or https://. The dot at the end of a sentence is not part of the address.

Input
See the site https://www.example.com and this one: http://www.test.com. The documents are at https://docs.example.com/guide/start.
Output
https://www.example.com
http://www.test.com
https://docs.example.com/guide/start

4. Dates to ISO. Turn every date dd.mm.yyyy in the text into the form yyyy-mm-dd with one re.sub.

Input
Contract of 03.10.2026, payment by 15.10.2026.
Output
Contract of 2026-10-03, payment by 2026-10-15.

More tasks

  • Statement total. Take the amounts from the first card with a group for the euros and a group for the cents, add them up and print "Total: €560.45".
  • Hashtags. Extract all words that start with # from a post that mixes plain Latin letters and letters like "é". Think about what would happen with the re.ASCII flag.
  • No tags. Remove the HTML tags from <p>The deadline is <b>15.11</b>.</p> and print the clean text.
  • Phone numbers in text. Find all mobile numbers in a text and print them in the form +359 88 812 3456, with groups and re.sub.

Test

Check yourself

1. What does re.finditer(pattern, text) give?
2. The pattern is (\d{2})\.(\d{2})\.(\d{4}), and the text is 15.11.2026. What is m.group(2)?
3. What does re.sub(r"(\d+)\.(\d+)\.(\d+)", r"\2/\1", "05.03.2026") give?
4. Why does "<.+>" catch the whole text "<b>Price</b>" and not just "<b>"?
5. What does re.MULTILINE do?

Made by Ivo Gergov as part of Evo Tech's campaign for accessible programming education ·