Regular Expressions in Python: A Practical Guide

Regular expressions in Python for everyday jobs: extracting IDs from text, validating formats, cleaning and rewriting strings. Raw strings, search, match and fullmatch, named groups, greedy versus lazy, re.sub with a function, verbose patterns, and the pitfalls that make patterns slow or wrong.

GloryTecks Team

Training & Placement Faculty

10 December 2025 6 min read

Last updated: 24 September 2026

Share:

A regular expression is a small pattern language for text: "three capital letters, a dash, then digits" or "a date written as day, slash, month, slash, year". String methods handle fixed text such as a known prefix or a comma; regular expressions handle text that varies within a pattern. Python's re module provides them (Python docs: re), and a dozen features cover nearly every everyday job. This guide focuses on those, and on the mistakes that make patterns slow or wrong.

Always use raw strings

Regular expressions use backslashes heavily (\d for a digit, \b for a word boundary), and so do Python string literals. Write patterns as raw strings, r"\d+", so Python passes the backslashes through untouched instead of interpreting them first; the regex HOWTO recommends this for every pattern (Python regex HOWTO). Without the r, "\b" is a backspace character and the pattern silently matches nothing.

Finding things: search, findall and finditer

python
import re

text = "Refund INV-1042 and INV-98 (too short), then order ORD-555001 on 25/09/2026."
ORDER_ID = r"\b[A-Z]{3}-\d{4,6}\b"

print(re.search(ORDER_ID, text).group())
print(re.findall(ORDER_ID, text))
for m in re.finditer(ORDER_ID, text):
    print(m.group(), m.start(), m.end())
text
INV-1042
['INV-1042', 'ORD-555001']
INV-1042 7 15
ORD-555001 51 61

The pattern reads: a word boundary, three capital letters, a dash, four to six digits, a word boundary. INV-98 does not match because it has only two digits. search returns the first match as a match object (or None), findall returns every match as strings, and finditer yields match objects with positions, which is what you want when you need to know where each match was.

Validating: match versus fullmatch

python
PIN = r"[1-9]\d{5}"                    # Indian PIN code: six digits, not starting with 0
for candidate in ["500081", "5000812", "050081", "500 081"]:
    print(candidate, bool(re.match(PIN, candidate)), bool(re.fullmatch(PIN, candidate)))
text
500081 True True
5000812 True False
050081 False False
500 081 False False

re.match only anchors at the start, so it happily accepts 5000812, whose first six characters look valid. For validation, use re.fullmatch, which requires the whole string to match. This is the most common regex bug in validation code. Note that a pattern this simple checks the shape of a PIN code, not that the code exists.

Groups: pulling out the parts

Parentheses capture parts of a match. Named groups, (?P<name>...), make the result self-describing:

python
LOG = re.compile(r"(?P<date>\d{4}-\d{2}-\d{2}) (?P<level>INFO|WARN|ERROR) (?P<message>.*)")
m = LOG.match("2026-09-25 ERROR payment gateway timeout after 30s")
print(m.group("level"), "|", m.group("message"))
print(m.groupdict())
text
ERROR | payment gateway timeout after 30s
{'date': '2026-09-25', 'level': 'ERROR', 'message': 'payment gateway timeout after 30s'}

re.compile stores the pattern as an object you can reuse; Python caches recently used patterns anyway, so compiling is mostly about naming the pattern and keeping it in one place. groupdict() turns a matched line into a dictionary, a quick way to parse structured logs.

Greedy and lazy quantifiers

*, + and {m,n} are greedy: they match as much as possible and then back off. Adding ? makes them lazy, matching as little as possible (Python regex HOWTO):

python
html_ish = "<b>total</b> and <b>tax</b>"
print(re.findall(r"<b>(.*)</b>", html_ish))
print(re.findall(r"<b>(.*?)</b>", html_ish))
text
['total</b> and <b>tax']
['total', 'tax']

The greedy version swallowed everything between the first <b> and the last </b>. When a pattern captures too much, a lazy quantifier or a narrower character class such as [^<]* is usually the fix. For real HTML, use an HTML parser instead; see web scraping with BeautifulSoup.

Replacing: re.sub, with backreferences or a function

python
dates = "Due 25/09/2026, paid 03/10/2026."
print(re.sub(r"(\d{2})/(\d{2})/(\d{4})", r"\3-\2-\1", dates))

def mask(m):
    return m.group()[:2] + "*" * (len(m.group()) - 4) + m.group()[-2:]

print(re.sub(r"\b\d{10}\b", mask, "Call 9876543210 or 040-12345678"))
text
Due 2026-09-25, paid 2026-10-03.
Call 98******10 or 040-12345678

In the replacement string, \1, \2 and \3 refer to the captured groups, which makes reordering day, month and year a one-liner. Passing a function instead of a string gives full control: here each ten-digit number is masked, while the landline with a dash is left alone.

Readable patterns with VERBOSE

Long patterns are hard to read on one line. The re.VERBOSE flag ignores whitespace in the pattern and allows comments, so a pattern can be laid out and explained:

python
IFSC = re.compile(r"""
    ^[A-Z]{4}      # bank code
    0              # reserved character, always zero
    [A-Z0-9]{6}$   # branch code
""", re.VERBOSE)
print([bool(IFSC.match(c)) for c in ["SBIN0001234", "SBIN1001234", "HDFC0ABC123"]])
text
[True, False, True]

Other useful flags are re.IGNORECASE and re.MULTILINE, which makes ^ and $ match at every line in a multi-line string (Python docs: re).

Pitfalls

  • The digit class matches more than 0 to 9. In string patterns, \d matches any Unicode decimal digit, including Arabic-Indic and Devanagari digits. Use [0-9] or the re.ASCII flag when only ASCII digits are acceptable.
  • User input inside a pattern. Characters such as ., + and ( have special meanings; wrap untrusted text with re.escape() before building a pattern from it.
  • Catastrophic backtracking. Nested quantifiers such as (a+)+$ can take exponential time on inputs that almost match, which is a known denial-of-service risk in web applications (OWASP: ReDoS). Keep patterns specific and avoid quantifiers nested inside quantified groups.
  • The wrong tool. JSON, HTML, CSV and dates all have parsers in the standard library or popular packages. They handle edge cases a pattern will not.
python
print(bool(re.fullmatch(r"\d{6}", "५००००१")), bool(re.fullmatch(r"[0-9]{6}", "५००००१")))
print(re.escape("price (INR) 1.5+"))
text
True False
price\ \(INR\)\ 1\.5\+

The first line shows \d accepting six Devanagari digits that [0-9] rejects.

When to reach for re, and when not

Use string methods when the text is fixed: startswith, in, split, replace are simpler and faster; see Python string methods. Use re when the text follows a pattern that varies, such as IDs, codes, dates in text, or log lines. Use a real parser when the text is a structured format. Test every pattern against examples that should match and examples that should not, as the PIN code check above does.

What is the difference between re.search and re.match?

search looks for the pattern anywhere in the string; match only at the beginning. For validating a whole string, use fullmatch.

Should I compile my patterns?

It is optional for performance, because the re module caches recently used patterns. Compile patterns you use in several places, so each has one name and one definition.

Why does the dot not match across lines?

By default the dot matches any character except a newline. Add the re.DOTALL flag when a pattern must span several lines, or use a character class such as [\s\S] for that one spot.

What does a word boundary match?

The position between a word character (a letter, digit or underscore) and a non-word character, or the start or end of the text. It matches no characters itself, which is why it stops INV-1042 from matching inside XINV-10423.

Can a regular expression validate an email address?

Only roughly. Fully correct validation is far more complex than any practical pattern, so check for a basic shape and confirm the address by sending an email to it.

Regular expressions show up constantly in data cleaning; the automation scripts guide uses one to pull email addresses out of text files. Regular expressions and pattern matching are part of the strings module of the Python Programming course.

Sources

GloryTecks Team

Training & Placement Faculty

The GloryTecks faculty team mentors learners across Data Science, AI and Data Engineering, with a focus on real-time projects and placement readiness in Hyderabad.

Want to master Python?

Join GloryTecks’ expert-led Python Programming training in Hyderabad with real-time projects and 100% placement support.

Explore course