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)Truefullmatch 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())€450.00€87.35€23.10re.compile(...)turns the pattern into aPatternobject. 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 afor: each iteration gets the next one, until they run out.mis aMatchobject: one match that was found.m.group()is its text.
More precisely, if you are curious
re.findallgives you a list with the texts of the matches right away, without theMatchobjects.re.searchgives only the first match, wherever it is, orNone.re.matchchecks only whether the text starts with a match: for "Table 12" it givesNone, even though 12 is in the text.re.sub,re.splitandre.fullmatchcompile the pattern themselves and keep the last few patterns ready.re.compileis 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())['€450.00', '€87.35', '€23.10']None12See 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 = 0for m in re.finditer("ERROR", log): count += 1 print(f"{m.group()} from {m.start()} to {m.end()}")print(f"Total: {count}")ERROR from 0 to 5ERROR from 24 to 29Total: 2m.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()\d+ is "one or more digits", so "12" is one match. With \d and no +, each digit would be separate: [1][2][4][2]. The space and the comma break the run, so 12 and 4 do not merge.
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))15.11.202615 / 11 / 20262026-11-15- Groups are numbered by their opening parentheses, from left to right, starting at 1.
m.group(1)is group 1.group(0)orgroup()is the whole match.searchis for when you need only the first match. If there is none,misNoneandif 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
- The date pattern has three pairs of parentheses: three groups.
- search looks for the pattern in the text. If it finds it, m is the match (Match), otherwise None.
- search finds the date: from index 5 to 15.
- Each group remembers its part: group 1 is 15, group 2 is 11, group 3 is 2026.
- The parts sit in m, each under its own number. group(0) is the whole match.
- group() is the same as group(0): 15.11.2026.
- The three groups, one by one.
- 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.groupsgives 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 isNone.
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))35 NoneThe idea
Named groups and replacing
(?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}")Coffee with milk: €6.4Croissant: €2.9re.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))Deadline: 2026-11-15, reminder: 2026-11-08Deadline: 11/15/2026, reminder: 11/08/2026re.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.submay stop with an error. Double the backslashes in it withreplace("\\", "\\\\"):
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"))bad escape \T at position 2Copy in C:\Temp- You can also use a group in the pattern itself:
\1means "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())One match, the whole line. .+ takes as much as it can: it goes to the end of the text, then gives back character by character until it finds a >. The first > from the back is the last one in the line. Quantifiers like this are called greedy.
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))<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.
search finds only the first match. For all of them you need for m in order.finditer(...): each iteration carries on from where the previous one stopped.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))No connection to the databaseDisk 95%re.IGNORECASEis the same as(?i): upper and lower case letters are the same.re.MULTILINEmakes^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.DOTALLlets.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)Repeated: caféRepeated: pleaseFalse 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.
This is old content. More old content, not the oldest.This is new content. More new content, not the oldest.Replaced: 22. Words with a prefix. The first line is the prefix, the second is the text. Print each word that starts with the prefix.
appapple application banana appreciate snappleappleapplicationappreciate3. 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.
See the site https://www.example.com and this one: http://www.test.com. The documents are at https://docs.example.com/guide/start.https://www.example.comhttp://www.test.comhttps://docs.example.com/guide/start4. Dates to ISO. Turn every date dd.mm.yyyy in the text into the form yyyy-mm-dd with one re.sub.
Contract of 03.10.2026, payment by 15.10.2026.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 there.ASCIIflag. - 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 andre.sub.
Test