Skip to content
1stepGrowLearnCompareGrow

Regular Expressions in Python, Part 1: Syntax, Functions, Flags

Regular expressions in Python from the ground up: raw strings, core syntax, match vs search vs fullmatch, flags, and five mistakes that silently break patterns.

Ayushi Kulshreshta

AI Engineer

9 min readUpdated
Share
On this page

Regular expressions in Python live in the built-in re module, and they let you search, extract, validate and replace text by pattern rather than by exact string. Most real work needs only raw strings, a handful of syntax elements and four functions. Get one of those wrong, though, and a pattern fails without an error: re.search("\bword\b", ...) quietly returns None.

This first part is for Python users who can write a loop but find regex write-only. It covers the syntax tables, which function to call when, the flags, a set of practical patterns, and the five mistakes behind most broken regex. Part 2 covers advanced Python regex: named groups, lookarounds and substitution with functions.

Why should you always use raw strings?

Start here, because it prevents a whole class of confusing failures.

Python
import re

# wrong - Python turns \b into a backspace character before regex sees it
print(re.search("\bword\b", "a word here"))     # None

# right
print(re.search(r"\bword\b", "a word here"))    # <re.Match object; span=(2, 6), match='word'>

In a normal string, Python interprets backslash escapes first. \d happens to survive because it is not a recognised escape, which lulls people into thinking it is fine. Then \b, \n or \t silently changes meaning.

Newer versions make the problem louder. According to the What's New in Python 3.12 notes, an invalid escape such as "\d" now emits a SyntaxWarning, and a future version will raise SyntaxError. As of September 2026, the current stable release, Python 3.14.7, still warns rather than fails. The Regular Expression HOWTO calls this "the backslash plague" and gives the same fix.

Write every pattern as r"...". It costs nothing.

What is the core regex syntax?

These tables cover most of what you will use day to day. The full list is in the re module syntax reference.

Character classes

Pattern Matches
\d / \D A digit / any non-digit
\w / \W A word character (letters, digits, underscore) / anything else
\s / \S Whitespace / any non-whitespace
. Any character except a newline
[abc] Any one of a, b or c
[^abc] Any character except a, b or c
[a-z] Any character in the range

One detail most cheat sheets get wrong: for normal str patterns, \w and \d are Unicode-aware. re.findall(r"\w+", "naïve café") returns ['naïve', 'café'], and \d matches Devanagari or Arabic digits too. Add flags=re.ASCII if you really mean only [a-zA-Z0-9_] and [0-9].

Quantifiers

Pattern Meaning
* Zero or more
+ One or more
? Zero or one (optional)
{3} Exactly 3
{2,5} Between 2 and 5
{2,} 2 or more

Anchors

Pattern Meaning
^ Start of string (or of each line, with re.MULTILINE)
$ End of string, or just before a final newline (or end of each line, with re.MULTILINE)
\b Word boundary

How do match, search, findall and finditer differ?

Python
text = "Order 12345 shipped on 2026-07-30, order 67890 pending"

re.search(r"\d+", text)      # <re.Match object; span=(6, 11), match='12345'>
re.match(r"Order", text)     # <re.Match object; span=(0, 5), match='Order'>
re.match(r"\d+", text)       # None - the string does not START with a digit
re.findall(r"\d+", text)     # ['12345', '2026', '07', '30', '67890']
re.finditer(r"\d+", text)    # iterator of match objects, with positions

The match versus search distinction causes more confusion than anything else in the module. match is anchored at position zero, whereas search scans. The re documentation has a whole section on search() vs. match(). Use search unless you genuinely mean "must begin with".

finditer is the lazy option: it yields one match object at a time, so it suits long texts. The same idea is explained in iterators and generators in Python.

Working with a match object:

Python
m = re.search(r"(\d{4})-(\d{2})-(\d{2})", text)
if m:
    print(m.group(0))    # 2026-07-30 - the whole match
    print(m.group(1))    # 2026 - first group
    print(m.groups())    # ('2026', '07', '30')
    print(m.span())      # (23, 33) - start and end positions

Always check for None before using the result. Calling re.search(...).group() on a non-match raises AttributeError: 'NoneType' object has no attribute 'group', and it is the most common regex crash in production code.

What is the difference between greedy and lazy quantifiers?

A greedy quantifier takes as much text as it can, while a lazy one (written with a trailing ?) takes as little as it can. Quantifiers are greedy by default:

Python
html = "<b>bold</b> and <i>italic</i>"

re.findall(r"<.+>", html)    # ['<b>bold</b> and <i>italic</i>']  - one giant match
re.findall(r"<.+?>", html)   # ['<b>', '</b>', '<i>', '</i>']     - lazy

The ? after + or * makes it lazy, so it matches as little as possible. This matters any time you are extracting between delimiters.

Often, however, the better fix is to be more specific rather than lazy:

Python
re.findall(r"<[^>]+>", html)   # ['<b>', '</b>', '<i>', '</i>']

[^>]+ says "anything that is not a closing bracket", which cannot overshoot in the first place. Negated character classes are usually clearer than lazy quantifiers, and they give the engine less backtracking to do.

Which regex flags should you know?

Flags change how a pattern behaves. Pass them by keyword:

Flag Effect Example result
re.IGNORECASE Case-insensitive matching findall(r"order \d+", text, flags=re.IGNORECASE) finds both orders
re.MULTILINE ^ and $ match at every line findall(r"^\w+", "one\ntwo", flags=re.MULTILINE) gives ['one', 'two']
re.DOTALL . also matches a newline findall(r"a.b", "a\nb", flags=re.DOTALL) gives ['a\nb']
re.ASCII \w, \d, \s match ASCII only findall(r"\w+", "café", flags=re.ASCII) gives ['caf']
re.VERBOSE Ignore whitespace, allow comments See the readable pattern below

Combine flags with |, for example flags=re.IGNORECASE | re.MULTILINE.

Practical examples of regular expressions in Python

Extracting numbers, including decimals and negatives:

Python
text = "Revenue was 45.2M, down -3.1% from 48.3M"
re.findall(r"-?\d+(?:\.\d+)?", text)     # ['45.2', '-3.1', '48.3']

The (?:\.\d+)? part makes the decimal portion optional as a unit, so a trailing full stop in "version 3." is not swallowed. The (?:...) is a non-capturing group, which Part 2 explains.

Splitting on multiple delimiters:

Python
re.split(r"[,;|]\s*", "a, b; c|d")   # ['a', 'b', 'c', 'd']

Validating a rough format:

Python
def looks_like_pincode(s: str) -> bool:
    return bool(re.fullmatch(r"[1-9]\d{5}", s))

looks_like_pincode("411001")   # True
looks_like_pincode("011001")   # False - cannot start with 0

fullmatch requires the entire string to match, which is what you almost always want for validation. Using search for validation is a classic mistake, because re.search(r"\d{6}", "abc411001xyz") succeeds.

Cleaning whitespace:

Python
re.sub(r"\s+", " ", "too   much\n\nspace").strip()   # 'too much space'

Should you compile regex patterns?

Compile a pattern when you reuse it, mainly for readability rather than speed. If a pattern runs in a loop, compile it once:

Python
pattern = re.compile(r"\b[A-Z]{2,}\b")

pattern.findall("The API returned JSON via HTTP")   # ['API', 'JSON', 'HTTP']

The re documentation notes that Python caches the most recently used patterns, so the speed gain is modest. Even so, a compiled pattern gives you a named object, which reads better than a repeated literal.

Readable patterns with re.VERBOSE

Anything non-trivial should use re.VERBOSE, which ignores whitespace and allows comments:

Python
date_re = re.compile(r"""
    (?P<year>\d{4})                  # four-digit year
    -
    (?P<month>0[1-9]|1[0-2])         # 01 through 12
    -
    (?P<day>0[1-9]|[12]\d|3[01])     # 01 through 31
""", re.VERBOSE)

m = date_re.search("due 2026-07-30")
m.group("year")     # '2026'
m.groupdict()       # {'year': '2026', 'month': '07', 'day': '30'}

Named groups make the extraction self-documenting. This version is longer than the cryptic equivalent and considerably easier to fix in six months.

Common regex mistakes in Python

  • Passing flags positionally. re.sub(r"cat", "dog", "Cat cat CAT", re.IGNORECASE) returns 'Cat dog CAT', because the fourth positional argument is count, and re.IGNORECASE equals 2. Write flags=re.IGNORECASE, which returns 'dog dog dog'. Since Python 3.13, the positional form also raises a DeprecationWarning.
  • Validating with $ instead of fullmatch. $ also matches just before a trailing newline, so re.match(r"\d{6}$", "411001\n") succeeds while re.fullmatch(r"\d{6}", "411001\n") does not.
  • Assuming \w means ASCII. It matches accented and non-Latin letters unless you add re.ASCII.
  • Calling .group() without checking for None.
  • Forgetting the r prefix.

When should you not use regex?

For genuinely structured formats, use a parser: json, csv, an HTML library or a date parser. Regex on HTML is the canonical example of a solution that works on your test cases and fails on real input.

Email validation is another trap, because a fully correct pattern is notoriously enormous. Check for an @ with something either side, then send a confirmation email.

The Sunday Growth Brief

One email a week: the best new comparisons, a fresh roadmap and the tech news worth your attention.

No spam. Unsubscribe in one click.

Advanced Python regex (Part 2) covers groups, lookarounds and substitution. Python strings covers the non-regex string methods, which are faster and clearer for fixed text. If a regex crash reaches production, exception handling in Python shows how to catch it narrowly.

Frequently asked questions

Why do regex patterns need the r prefix?

Because Python processes backslash escapes in normal strings first. The sequence '\b' becomes a backspace character and silently breaks a word-boundary pattern. Unrecognised escapes such as '\d' still work today, but since Python 3.12 they trigger a SyntaxWarning and are due to become errors. A raw string passes backslashes through untouched, so always use r'...'.

What is the difference between match, search and findall?

re.match only checks at the start of the string, re.search scans for the first match anywhere, and re.findall returns every non-overlapping match as a list of strings. re.fullmatch requires the whole string to match. Use search unless you specifically mean 'must start with', and fullmatch when you are validating input.

What does greedy mean in a regular expression?

Quantifiers such as * and + take as much text as they can while still allowing the overall pattern to match. Adding ? after them makes them lazy, so they take as little as possible. This matters most when matching between delimiters like HTML tags or quotes, where a greedy .+ runs straight past the first closing delimiter.

When should I not use regex?

Avoid regex for structured formats that have real parsers, such as HTML, JSON, CSV and dates. Use json, csv, an HTML parser or datetime instead. Full email validation is another poor fit. Regular expressions are best for finding patterns in unstructured or semi-structured text, such as log lines, IDs and codes.

Written by

Ayushi Kulshreshta

AI Engineer

AI engineer building retrieval-augmented systems, previously a software developer at VDB Inc. and an R&D associate at Nokia.

Retrieval-Augmented GenerationLangChainPythonMachine learning

Related reading

Free weekly newsletter

Get the shortlist before everyone else

Every Sunday we send one email with the week’s sharpest course comparison, a career roadmap worth stealing, and the tech news and hiring signals we’re watching.

  • No fluff, ever
  • Unsubscribe anytime
  • 5-minute read

We never share your address. One click to leave.