Regular Expressions

Python · Lesson 45 of 56 7 min read Updated

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.

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.

PythonRun it yourself
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))
Output
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:

PatternMatches
.Any single character except a newline
\dA digit, 0 to 9
\wA word character: letter, digit or underscore
\sA space, tab or newline
\D, \W, \SThe 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
\bA word boundary: the edge of a word

Quantifiers say how many times the item before them may repeat:

QuantifierMeaning
*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.

PythonRun it yourself
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'}")
Output
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.

PythonRun it yourself
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))
Output
['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.

PythonRun it yourself
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())
Output
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.

PythonRun it yourself
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"))
Output
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.

PythonRun it yourself
import re

line = 'He said "chai" and she said "coffee"'
print(re.findall(r'"(.+)"', line))
print(re.findall(r'"(.+?)"', line))
Output
['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 r prefix. 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. Use re.fullmatch() for whole values.
  • Not escaping special characters. In Rs. the dot matches any character; write Rs\. to mean a real full stop.
  • Using regex for simple jobs. text.startswith("+91") or "@" in email is clearer than a pattern. See String Methods.
  • Not checking for None. re.search(...).group() crashes with AttributeError when nothing matched. Test the match first.

Key points

  • Import re and write patterns as raw strings: r"\d+".
  • re.search() finds the first match, re.fullmatch() checks the whole text and re.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 with m["name"].
  • re.sub() replaces matches (use \1 for groups) and re.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
Python
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)
Output
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?

2. What does \d{3} match?

3. What does re.findall(r"#(\w+)", "#chai #samosa") return?

4. Why write regex patterns as raw strings like r"\d+"?

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