Regular Expressions
How would you check that a mobile number has exactly 10 digits and starts with 6, 7, 8 or 9? Or pull every rupee amount out of a bank SMS? You could write loops with string methods, but a regular expression (regex for short) does it in one line. A regex is a small pattern language that describes what text should look like.
Python supports regular expressions through the built-in re module. In this lesson you will learn the most useful pattern symbols and the functions that search, validate, extract, replace and split text.
Your first search
Import re, write a pattern, and call re.search(pattern, text). It looks for the first place where the pattern matches. If it finds one, it returns a match object; otherwise it returns None.
import re
sms = "Your OTP is 482913. Do not share it with anyone."
match = re.search(r"\d{6}", sms)
if match:
print("OTP found:", match.group())
print("Position:", match.start(), "to", match.end())
print(re.search(r"\d{8}", sms))OTP found: 482913 Position: 12 to 18 None
The pattern \d{6} means "a digit, exactly six times". match.group() returns the matched text. The second search wants eight digits in a row, finds none, and returns None.
The pattern language
Most characters in a pattern match themselves: the pattern Pune matches the text "Pune". Special characters add power:
| Pattern | Matches |
|---|---|
. | Any single character except a newline |
\d | A digit, 0 to 9 |
\w | A word character: letter, digit or underscore |
\s | A space, tab or newline |
\D, \W, \S | The opposite of \d, \w, \s |
[aeiou] | Any one character from the set |
[A-Z], [0-9] | Any one character in the range |
[^0-9] | Any one character that is NOT a digit |
^ and $ | The start and end of the text |
\b | A word boundary: the edge of a word |
Quantifiers say how many times the item before them may repeat:
| Quantifier | Meaning |
|---|---|
* | 0 or more times |
+ | 1 or more times |
? | 0 or 1 time (optional) |
{6} | Exactly 6 times |
{2,4} | Between 2 and 4 times |
A vertical bar means "or": cat|dog matches either word. To match a special character literally, put a backslash before it: \. matches a real full stop and \+ a real plus sign.
Validating input with fullmatch()
re.search() is happy if the pattern appears anywhere. To check that the whole text fits a pattern, use re.fullmatch(). It is the right tool for validating form fields.
import re
checks = {
"mobile": r"[6-9]\d{9}",
"pin code": r"[1-9]\d{5}",
"PAN": r"[A-Z]{5}\d{4}[A-Z]",
}
samples = {
"mobile": ["9876543210", "5876543210", "98765 43210"],
"pin code": ["124001", "024001"],
"PAN": ["ABCPK1234F", "abcpk1234f"],
}
for field, pattern in checks.items():
for value in samples[field]:
ok = re.fullmatch(pattern, value) is not None
print(f"{field:<9} {value:<12} {'valid' if ok else 'invalid'}")mobile 9876543210 valid mobile 5876543210 invalid mobile 98765 43210 invalid pin code 124001 valid pin code 024001 invalid PAN ABCPK1234F valid PAN abcpk1234f invalid
Read the mobile pattern piece by piece: [6-9] is one digit from 6 to 9, then \d{9} is nine more digits. The number with a space fails, because a space is not a digit. Such a pattern checks the format only; it cannot tell whether the number actually exists.
Finding all matches with findall()
re.findall() returns a list of every match in the text. It is great for extracting data.
import re
statement = """Paid Rs. 1,299 to Croma on 03-10-2026.
Received Rs. 15,000 salary on 01-10-2026.
Paid Rs. 249 to Zomato. #food #weekend"""
amounts = re.findall(r"Rs\. ([\d,]+)", statement)
print(amounts)
print(sum(int(a.replace(",", "")) for a in amounts))
print(re.findall(r"\d{2}-\d{2}-\d{4}", statement))
print(re.findall(r"#(\w+)", statement))['1,299', '15,000', '249'] 16548 ['03-10-2026', '01-10-2026'] ['food', 'weekend']
Round brackets create a group. When a pattern has one group, findall() returns only the part inside the brackets, which is why we get 1,299 instead of Rs. 1,299. The class [\d,]+ means "one or more digits or commas".
Groups and named groups
With several groups you can split a match into pieces. Give a group a name with (?P<name>...) and read it back by that name, which is clearer than counting brackets.
import re
pattern = r"(?P<state>[A-Z]{2})\s?(?P<district>\d{2})\s?(?P<series>[A-Z]{1,2})\s?(?P<number>\d{4})"
for plate in ["HR 12 AB 3456", "RJ14CX0042", "KL07 Z 9999"]:
m = re.fullmatch(pattern, plate)
if m:
print(m.group("state"), m["district"], m["series"], m["number"])
print(" ", m.groupdict())HR 12 AB 3456
{'state': 'HR', 'district': '12', 'series': 'AB', 'number': '3456'}
RJ 14 CX 0042
{'state': 'RJ', 'district': '14', 'series': 'CX', 'number': '0042'}
KL 07 Z 9999
{'state': 'KL', 'district': '07', 'series': 'Z', 'number': '9999'}The \s? parts allow an optional space, so all three ways of writing a vehicle number are accepted. m["district"] is a shorter way to write m.group("district").
Replacing and splitting
re.sub(pattern, replacement, text) replaces every match. In the replacement, \1, \2 and so on insert the text of each group. re.split() splits text wherever the pattern matches, which helps when the separators vary.
import re
message = "Call Priya on 9876543210 or Kabir on 9123456780"
tidy = re.sub(r"\s+", " ", message)
print(tidy)
masked = re.sub(r"(\d{2})\d{6}(\d{2})", r"\1XXXXXX\2", tidy)
print(masked)
print(re.split(r"\s*[,;/]\s*", "dosa, idli;vada / upma"))
word = re.compile(r"\bpython\b", re.IGNORECASE)
print(word.findall("Python, python3 and PYTHON"))Call Priya on 9876543210 or Kabir on 9123456780 Call Priya on 98XXXXXX10 or Kabir on 91XXXXXX80 ['dosa', 'idli', 'vada', 'upma'] ['Python', 'PYTHON']
re.compile() turns a pattern into a reusable object with the same methods, which is tidy when you use a pattern many times. The flag re.IGNORECASE ignores capital letters. The \b word boundaries stop python3 from counting, because after "python" comes "3", a word character, so there is no boundary there.
Greedy and lazy matching
Quantifiers are greedy: they match as much text as possible. Add ? after a quantifier to make it lazy, matching as little as possible.
import re
line = 'He said "chai" and she said "coffee"'
print(re.findall(r'"(.+)"', line))
print(re.findall(r'"(.+?)"', line))['chai" and she said "coffee'] ['chai', 'coffee']
The greedy .+ ran all the way to the last quote. The lazy .+? stops at the first closing quote it can.
Common mistakes
- Forgetting the
rprefix. Always write patterns as raw strings so backslashes reach the regex engine unchanged. - Using
search()to validate.re.search(r"\d{6}", "abc1234567")succeeds because six digits appear inside. Usere.fullmatch()for whole values. - Not escaping special characters. In
Rs.the dot matches any character; writeRs\.to mean a real full stop. - Using regex for simple jobs.
text.startswith("+91")or"@" in emailis clearer than a pattern. See String Methods. - Not checking for
None.re.search(...).group()crashes withAttributeErrorwhen nothing matched. Test the match first.
Key points
- Import
reand write patterns as raw strings:r"\d+". re.search()finds the first match,re.fullmatch()checks the whole text andre.findall()returns all matches.\d,\w,\s,., character sets like[A-Z]and quantifiers like+,*,?and{n}build patterns.- Round brackets make groups;
(?P<name>...)makes named groups you read withm["name"]. re.sub()replaces matches (use\1for groups) andre.split()splits on a pattern.- Quantifiers are greedy; add
?to make them lazy.
Exercise
A school in Jaipur receives roll entries like "Roll 23: Meera Sharma (meera.s@school.in)". Write a pattern with named groups roll, name and email, and use it to print each student's roll number and email from three such lines. Then use re.sub() to hide everything before the @ in each email, so meera.s@school.in becomes ***@school.in.
Show solution
import re
lines = [
"Roll 23: Meera Sharma (meera.s@school.in)",
"Roll 7: Farhan Ali (farhan.ali@school.in)",
"Roll 31: Ananya Iyer (ananya31@school.in)",
]
pattern = r"Roll (?P<roll>\d+): (?P<name>[A-Za-z ]+) \((?P<email>[\w.]+@[\w.]+)\)"
for line in lines:
m = re.fullmatch(pattern, line)
hidden = re.sub(r"^[^@]+", "***", m["email"])
print(m["roll"], m["name"], hidden)23 Meera Sharma ***@school.in 7 Farhan Ali ***@school.in 31 Ananya Iyer ***@school.in
The brackets around the email are escaped as \( and \) because plain brackets would create a group. In the masking pattern, ^[^@]+ means "from the start, one or more characters that are not @".
Quick Quiz
1. Which function checks that an entire string matches a pattern?
fullmatch() succeeds only if the pattern covers the whole text.
2. What does \d{3} match?
\d is a digit and {3} repeats it exactly three times.
3. What does re.findall(r"#(\w+)", "#chai #samosa") return?
With one group, findall() returns only the grouped part of each match.
4. Why write regex patterns as raw strings like r"\d+"?
In normal strings some backslash sequences become special characters before re sees them.
Learn Python with a mentor
Free lessons get you started. Our Python Programming training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
