EASYTUTORGUIDE

Practical tutorials, tools, courses, digital skills, and business promotion.

Free Learning

JavaScript – Chapter 2: Development Environment

Learn what JavaScript is, where it runs, why it is important, and how beginners can start using it with simple examples and clear output.

Beginner Friendly JavaScript Basics Web Development Code Examples
JavaScript Lesson 2 Chapter 2 Topics Day / Night Mode

Main reading content

python-course-chapter-29

Chapter 29: Regular Expressions

A complete beginner-friendly guide to finding, validating, extracting, splitting, and replacing text with Python regular expressions.

Goal: Understand regular-expression patterns, special characters, matching methods, validation concepts, and practical text-processing applications using Python's built-in re module.

Chapter 29 Topics

29.1 Introduction to Regular Expressions

```

A regular expression, often called regex, is a pattern used to search, test, extract, split, or replace text. Instead of checking every character manually, you describe the kind of text you want to find. Python then searches the larger string for text that follows that pattern.

Regular expressions are useful for finding phone numbers, email addresses, dates, postal codes, repeated words, product numbers, and many other text formats. They are powerful, but beginners should build patterns gradually and test each part carefully.

Example

import re
```

text = "My order number is 58321."

# Search for one or more digits

result = re.search(r"\d+", text)

if result:
print("Number found:", result.group())
else:
print("No number was found.")
```

Output

Number found: 58321

Output Explanation

The pattern \d+ means one or more digits. Python searches the sentence and finds the number 58321. The group() method returns the exact text that matched the pattern.

```

29.2 The re Module

```

Python provides regular-expression tools through the built-in re module. You must import this module before using functions such as search(), match(), fullmatch(), findall(), finditer(), split(), and sub().

The module also provides flags that change matching behavior and compile() for creating reusable pattern objects. Because re is built into Python, no separate installation is required.

Example

import re
```

sentence = "Python is easy to learn."

result = re.search("easy", sentence)

if result:
print("Match:", result.group())
print("Start position:", result.start())
print("End position:", result.end())
```

Output

Match: easy
```

Start position: 10
End position: 14
```

Output Explanation

The search() function returns a match object. The group() method shows the matched text. The start position is included, while the end position points immediately after the match.

```

29.3 Creating Patterns

```

A regex pattern is a string that describes what the program should find. A simple pattern may contain ordinary letters, while a more advanced pattern may include symbols for digits, spaces, repeated characters, optional text, or alternative choices.

Patterns should be created from small understandable parts. For example, a simple product code may contain three uppercase letters, a hyphen, and four digits. Each part can be represented separately and then combined.

Example

import re
```

product_code = "ABC-4821"

# Three uppercase letters, one hyphen, and four digits

pattern = r"[A-Z]{3}-\d{4}"

result = re.fullmatch(pattern, product_code)

if result:
print("Valid product code")
else:
print("Invalid product code")
```

Output

Valid product code

Output Explanation

[A-Z]{3} requires three uppercase letters. The hyphen is matched literally. \d{4} requires exactly four digits. Because the entire value follows the pattern, fullmatch() succeeds.

```

29.4 Raw Strings

```

Regular expressions often use backslashes, such as \d for a digit or \s for whitespace. Python strings also use backslashes for escape sequences. This can create confusion because both Python and regex may try to interpret the same backslash.

A raw string begins with the letter r. It tells Python to treat backslashes more literally. Regex patterns should normally be written as raw strings because they are clearer and require fewer doubled backslashes.

Example

import re
```

text = "The code is 7429."

# Recommended raw-string pattern

pattern = r"\d{4}"

result = re.search(pattern, text)

print(result.group())
```

Output

7429

Output Explanation

The raw string keeps \d easy to read. The pattern requests exactly four digits and finds 7429. Without the raw-string prefix, some patterns may require additional escaping.

```

29.5 Literal Characters

```

Literal characters match themselves. For example, the pattern cat looks for the letters c, a, and t in that exact order. Most letters and numbers behave as literal characters unless a special regex meaning is assigned to them.

Some punctuation symbols are metacharacters and must be escaped when you want their literal meaning. For example, a literal period is written as \. because an unescaped period means any character.

Example

import re
```

text = "Visit example.com for more information."

# The period is escaped to match a real period

pattern = r"example.com"

result = re.search(pattern, text)

if result:
print("Website found:", result.group())
```

Output

Website found: example.com

Output Explanation

The letters match themselves. The escaped period matches the actual dot between example and com. Without the backslash, the period could match any single character.

```

29.6 Metacharacters

```

Metacharacters are symbols with special meanings in regular expressions. Common examples include the period, caret, dollar sign, asterisk, plus sign, question mark, braces, brackets, parentheses, vertical bar, and backslash.

These symbols describe patterns rather than matching themselves. For example, a period matches almost any one character, a plus sign means one or more repetitions, and square brackets describe a set of allowed characters.

Common Metacharacters

  • . – Almost any single character
  • ^ – Beginning of text or line
  • $ – End of text or line
  • * – Zero or more repetitions
  • + – One or more repetitions
  • ? – Zero or one repetition
  • [] – Character class
  • () – Group
  • | – Alternative choice

Example

import re
```

words = ["cat", "cot", "cut", "coat"]

pattern = r"c.t"

for word in words:
if re.fullmatch(pattern, word):
print(word)
```

Output

cat
```

cot
cut
```

Output Explanation

The period matches one character between c and t. The word coat does not match because it contains two characters between the first and final letters.

```

29.7 Character Classes

```

A character class describes a set of possible characters at one position. Square brackets create a custom class, such as [abc]. Ranges such as [A-Z] and [0-9] can describe larger groups.

Python regex also provides shorthand classes. \d matches a digit, \w matches a word character, and \s matches whitespace. Uppercase versions such as \D, \W, and \S match the opposite.

Example

import re
```

text = "Room numbers: A12, B8, and C305."

# One uppercase letter followed by one or more digits

pattern = r"[A-Z]\d+"

rooms = re.findall(pattern, text)

print(rooms)
```

Output

['A12', 'B8', 'C305']

Output Explanation

[A-Z] matches one uppercase letter. \d+ matches one or more digits after it. The pattern therefore finds all three room numbers.

```

29.8 Quantifiers

```

Quantifiers specify how many times the previous character, class, or group may repeat. The asterisk means zero or more, the plus sign means one or more, and the question mark means zero or one.

Curly braces allow exact or limited repetition. For example, {4} means exactly four times, {2,5} means from two to five times, and {3,} means at least three times.

Example

import re
```

values = ["7", "42", "583", "9012", "12345"]

# Match numbers containing two to four digits

pattern = r"\d{2,4}"

for value in values:
if re.fullmatch(pattern, value):
print(value)
```

Output

42
```

583
9012
```

Output Explanation

The value must contain at least two digits and no more than four digits. The one-digit and five-digit values are rejected by fullmatch().

```

29.9 Anchors

```

Anchors match positions instead of visible characters. The caret ^ represents the beginning of a string or line, while the dollar sign $ represents the end.

Anchors are useful for validation because they prevent extra text from appearing before or after the required pattern. However, fullmatch() is often clearer when the entire input must follow one pattern.

Example

import re
```

usernames = ["user_25", "my-user", "123abc", "python99"]

# Username must start with a letter and contain only letters,

# digits, or underscores

pattern = r"^[A-Za-z][A-Za-z0-9_]*$"

for username in usernames:
if re.search(pattern, username):
print(username, "- valid")
else:
print(username, "- invalid")
```

Output

user_25 - valid
```

my-user - invalid
123abc - invalid
python99 - valid
```

Output Explanation

The caret requires the first character to be a letter. The middle class allows letters, digits, or underscores. The dollar sign ensures that nothing else appears at the end.

```

29.10 Groups

```

Parentheses create groups in a regular expression. A group allows several pattern parts to be treated as one unit. Quantifiers, alternatives, and extraction rules can then apply to the complete group.

Groups are useful when a repeated section contains multiple characters. For example, the group (ab)+ matches ab, abab, and longer repetitions of the same two-character unit.

Example

import re
```

values = ["ha", "haha", "hahaha", "hah"]

pattern = r"(ha)+"

for value in values:
if re.fullmatch(pattern, value):
print(value)
```

Output

ha
```

haha
hahaha
```

Output Explanation

The group contains the two letters ha. The plus sign applies to the complete group, so one or more complete repetitions are accepted. The value hah ends with an incomplete group.

```

29.11 Capturing Groups

```

A capturing group saves the text matched by a section inside parentheses. After a successful match, the saved sections can be accessed with group(1), group(2), and higher numbers.

Capturing groups are useful when a complete value contains separate meaningful parts. A date, for example, may contain a year, month, and day that should be extracted individually.

Example

import re
```

date_text = "2026-07-19"

pattern = r"(\d{4})-(\d{2})-(\d{2})"

result = re.fullmatch(pattern, date_text)

if result:
print("Complete date:", result.group(0))
print("Year:", result.group(1))
print("Month:", result.group(2))
print("Day:", result.group(3))
```

Output

Complete date: 2026-07-19
```

Year: 2026
Month: 07
Day: 19
```

Output Explanation

Group zero contains the complete match. Groups one, two, and three contain the captured year, month, and day. Each pair of parentheses creates one numbered capturing group.

```

29.12 Non-Capturing Groups

```

A non-capturing group uses the syntax (?:...). It groups pattern parts without saving the matched text as a numbered group. This is useful when grouping is needed only for repetition or alternatives.

Non-capturing groups keep captured results cleaner and prevent unnecessary group numbers. They behave like ordinary groups for matching but do not appear in the captured-group collection.

Example

import re
```

phone = "416-555-7821"

# The area code is grouped but not captured separately

pattern = r"(?:416|647)-(\d{3})-(\d{4})"

result = re.fullmatch(pattern, phone)

if result:
print("Complete match:", result.group(0))
print("Exchange:", result.group(1))
print("Line number:", result.group(2))
```

Output

Complete match: 416-555-7821
```

Exchange: 555
Line number: 7821
```

Output Explanation

The first group selects either area code 416 or 647, but ?: prevents it from becoming a numbered capture. Therefore, group one contains the exchange and group two contains the final four digits.

```

29.13 Alternation

```

Alternation uses the vertical bar | to represent different choices. It works like the word “or.” For example, cat|dog matches either cat or dog.

Parentheses are often used to control which parts belong to the alternatives. Without correct grouping, the alternatives may apply to more or less of the pattern than intended.

Example

import re
```

text = "We sell pizza, pasta, and salad."

pattern = r"pizza|burger|salad"

matches = re.findall(pattern, text)

print(matches)
```

Output

['pizza', 'salad']

Output Explanation

The pattern asks Python to find any of the three listed words. The sentence contains pizza and salad, but it does not contain burger.

```

29.14 Lookaheads

```

A lookahead checks what appears after the current position without including that later text in the match. A positive lookahead uses (?=...), while a negative lookahead uses (?!...).

Lookaheads are useful when a value must be followed by a certain pattern. For example, you can find numbers followed by the word dollars without including the word in the returned match.

Example

import re
```

text = "The shirt costs 25 dollars and the shoes cost 80 dollars."

# Match digits only when followed by a space and dollars

pattern = r"\d+(?=\s+dollars)"

prices = re.findall(pattern, text)

print(prices)
```

Output

['25', '80']

Output Explanation

The lookahead confirms that each number is followed by whitespace and the word dollars. Only the digits are returned because the lookahead checks the following text without consuming it.

```

29.15 Lookbehinds

```

A lookbehind checks what appears before the current position without including that earlier text in the match. A positive lookbehind uses (?<=...), while a negative lookbehind uses (?<!...).

Python lookbehinds normally require a fixed-width pattern. They are useful when extracting text that must follow a known prefix, such as a currency symbol or label.

Example

import re
```

text = "Keyboard: $45, Monitor: $250, Mouse: $20"

# Find numbers that appear immediately after a dollar sign

pattern = r"(?<=$)\d+"

prices = re.findall(pattern, text)

print(prices)
```

Output

['45', '250', '20']

Output Explanation

The positive lookbehind confirms that a dollar sign appears immediately before the digits. The dollar sign itself is not included in the returned values.

```

29.16 Greedy Matching

```

Regex quantifiers are greedy by default. This means they try to match as much text as possible while still allowing the complete pattern to succeed. Greedy behavior is useful in many situations, but it may capture more text than expected.

The quantifiers *, +, ?, and brace quantifiers normally use greedy behavior. You should examine their effect carefully when the same opening and closing characters appear several times.

Example

import re
```

text = "First and Second"

# Greedy matching

pattern = r".*"

result = re.search(pattern, text)

print(result.group())
```

Output

<b>First</b> and <b>Second</b>

Output Explanation

The .* section matches as much as possible. It starts at the first opening tag and continues to the final closing tag, so both bold sections and the text between them are included.

```

29.17 Lazy Matching

```

Lazy matching, also called non-greedy matching, tries to match as little text as possible. A question mark placed after a quantifier changes it from greedy to lazy. Examples include *?, +?, and {2,5}?.

Lazy matching is useful when you want separate matches between repeated opening and closing markers. It stops at the earliest position that allows the pattern to succeed.

Example

import re
```

text = "First and Second"

# Lazy matching

pattern = r".*?"

results = re.findall(pattern, text)

print(results)
```

Output

['<b>First</b>', '<b>Second</b>']

Output Explanation

The lazy .*? section stops at the first available closing tag. This allows Python to find two separate bold sections instead of one large match.

```

29.18 search()

```

The re.search() function scans through the string and returns the first location where the pattern matches. The match may begin anywhere in the text.

When a match is found, the function returns a match object. When nothing matches, it returns None. You should normally test the result before calling match-object methods.

Example

import re
```

text = "Customer ID: CUS-48291"

result = re.search(r"CUS-\d+", text)

if result:
print("Found:", result.group())
print("Position:", result.span())
else:
print("Customer ID not found")
```

Output

Found: CUS-48291
```

Position: (13, 22)
```

Output Explanation

The pattern begins after other text, but search() scans the complete string. The span() method returns a tuple containing the starting and ending positions.

```

29.19 match()

```

The re.match() function checks for a pattern only at the beginning of the string. It does not search later positions. This makes it different from search().

A successful beginning is enough for match(). Extra characters may appear after the matched part. Use fullmatch() when the complete string must follow the pattern.

Example

import re
```

first_text = "Python is powerful."
second_text = "I am learning Python."

pattern = r"Python"

first_result = re.match(pattern, first_text)
second_result = re.match(pattern, second_text)

print("First text:", bool(first_result))
print("Second text:", bool(second_result))
```

Output

First text: True
```

Second text: False
```

Output Explanation

The first sentence begins with Python, so it matches. The second sentence contains the word later, but match() checks only the beginning and therefore returns None.

```

29.20 fullmatch()

```

The re.fullmatch() function succeeds only when the complete string follows the pattern. It is especially useful for validating user input because extra text before or after the expected format causes the match to fail.

This method often removes the need to use beginning and ending anchors manually. It clearly communicates that the entire value must be valid.

Example

import re
```

postal_codes = ["L4C 5W6", "M5V 2T6", "L4C5W6", "123 456"]

pattern = r"[A-Z]\d[A-Z] \d[A-Z]\d"

for postal_code in postal_codes:
if re.fullmatch(pattern, postal_code):
print(postal_code, "- valid")
else:
print(postal_code, "- invalid")
```

Output

L4C 5W6 - valid
```

M5V 2T6 - valid
L4C5W6 - invalid
123 456 - invalid
```

Output Explanation

The pattern requires the Canadian letter-digit-letter, space, digit-letter-digit format. Missing spaces or incorrect character types cause the complete match to fail.

```

29.21 findall()

```

The re.findall() function returns all non-overlapping matches as a list. If no matches are found, it returns an empty list. This function is convenient when only the matched text is needed.

When the pattern contains capturing groups, the returned list may contain only the captured groups rather than the complete match. Use non-capturing groups when grouping is needed but complete matches should be returned.

Example

import re
```

text = "Scores: Ali 85, Sara 92, Michael 78."

scores = re.findall(r"\d+", text)

print(scores)
```

Output

['85', '92', '78']

Output Explanation

The pattern finds every sequence containing one or more digits. The results are returned as strings because regex works with text.

```

29.22 finditer()

```

The re.finditer() function returns an iterator of match objects. Each match object provides the matched text, position, captured groups, and other details.

It is useful when you need both the matches and their locations. Because it returns an iterator, it can also process many matches gradually instead of building a complete list immediately.

Example

import re
```

text = "Order 101, Order 205, Order 309"

matches = re.finditer(r"\d+", text)

for match in matches:
print(
"Value:",
match.group(),
"Start:",
match.start(),
"End:",
match.end()
)
```

Output

Value: 101 Start: 6 End: 9
```

Value: 205 Start: 17 End: 20
Value: 309 Start: 28 End: 31
```

Output Explanation

Each match object contains one order number and its position. The iterator provides the matches one at a time to the loop.

```

29.23 split()

```

The re.split() function divides a string wherever the regex pattern matches. It is more flexible than the normal string split() method because several different separators can be described with one pattern.

This is useful when text may use commas, semicolons, spaces, tabs, or other separators. The optional maxsplit argument limits the number of divisions.

Example

import re
```

text = "apple,banana;orange grape"

# Split on a comma, semicolon, or one or more spaces

items = re.split(r"[,;\s]+", text)

print(items)
```

Output

['apple', 'banana', 'orange', 'grape']

Output Explanation

The character class accepts commas and semicolons, while \s accepts whitespace. The plus sign allows one or more separator characters.

```

29.24 sub()

```

The re.sub() function replaces text that matches a pattern. It receives a pattern, replacement value, and source string. By default, every non-overlapping match is replaced.

The replacement may be ordinary text or a function. A function replacement can examine each match and create a different value. The optional count argument limits the number of replacements.

Example

import re
```

text = "Call 416-555-7821 or 647-555-9012."

# Replace phone numbers with a privacy label

hidden_text = re.sub(
r"\d{3}-\d{3}-\d{4}",
"[PHONE HIDDEN]",
text
)

print(hidden_text)
```

Output

Call [PHONE HIDDEN] or [PHONE HIDDEN].

Output Explanation

Both phone numbers follow the three-digit, three-digit, four-digit pattern. The function replaces each complete match with the privacy label.

```

29.25 Compiling Patterns

```

The re.compile() function creates a reusable regex pattern object. This can make code clearer when the same pattern is used several times. The compiled object provides methods such as search(), fullmatch(), and findall().

Compiling is useful when a pattern has a meaningful role, such as validating product numbers or extracting phone numbers. The object can be given a descriptive variable name.

Example

import re
```

product_pattern = re.compile(r"[A-Z]{2}-\d{5}")

codes = [
"AB-12345",
"XY-98765",
"A-12345",
"AB-1234"
]

for code in codes:
if product_pattern.fullmatch(code):
print(code, "- valid")
else:
print(code, "- invalid")
```

Output

AB-12345 - valid
```

XY-98765 - valid
A-12345 - invalid
AB-1234 - invalid
```

Output Explanation

The pattern object is created once and reused for every code. A valid code requires two uppercase letters, one hyphen, and five digits.

```

29.26 Regular Expression Flags

```

Regex flags change how matching behaves. They can make matching case-insensitive, allow anchors to work on separate lines, allow a period to match newline characters, or make complex patterns easier to format.

Flags can be passed to regex functions or to re.compile(). Several flags can be combined with the vertical bar operator.

Common Flags

  • re.IGNORECASE or re.I – Ignore uppercase and lowercase differences
  • re.MULTILINE or re.M – Apply anchors to each line
  • re.DOTALL or re.S – Allow the period to match newlines
  • re.VERBOSE or re.X – Allow spaces and comments in a pattern

Example: Ignore Case

import re
```

text = "Python, PYTHON, and python"

matches = re.findall(
r"python",
text,
flags=re.IGNORECASE
)

print(matches)
```

Output

['Python', 'PYTHON', 'python']

Example: Verbose Pattern

import re
```

phone_pattern = re.compile(
r"""
\d{3}      # Area code
-          # First hyphen
\d{3}      # Exchange
-          # Second hyphen
\d{4}      # Line number
""",
re.VERBOSE
)

print(bool(phone_pattern.fullmatch("416-555-7821")))
```

Output

True

Output Explanation

The first example matches the same word in several letter cases. The verbose pattern allows the phone-number pattern to be divided across lines with helpful comments.

```

29.27 Email Validation Concepts

```

Email validation checks whether an entered value follows a reasonable email structure. A basic check usually requires a local name, an at sign, a domain name, a period, and a final domain section.

Complete international email rules are extremely complicated. A beginner regex should check a practical format rather than attempting to implement every official possibility. Real applications should also verify ownership by sending a confirmation message.

Example

import re
```

email_pattern = re.compile(
r"[A-Za-z0-9._%+-]+"
r"@"
r"[A-Za-z0-9.-]+"
r"."
r"[A-Za-z]{2,}"
)

emails = [
"[user@example.com](mailto:user@example.com)",
"[first.last@mail.co](mailto:first.last@mail.co)",
"missing-at.com",
"user@domain",
"[name+tag@example.ca](mailto:name+tag@example.ca)"
]

for email in emails:
if email_pattern.fullmatch(email):
print(email, "- valid format")
else:
print(email, "- invalid format")
```

Output

user@example.com - valid format
```

[first.last@mail.co](mailto:first.last@mail.co) - valid format
missing-at.com - invalid format
user@domain - invalid format
[name+tag@example.ca](mailto:name+tag@example.ca) - valid format
```

Output Explanation

The pattern checks for allowed local-name characters, one at sign, a domain, a literal period, and at least two final letters. It checks formatting only and does not prove that the address exists.

```

29.28 Data Extraction

```

Data extraction means finding useful values inside larger unstructured text. Regex can extract dates, prices, identifiers, phone numbers, email addresses, and other information without reading every character manually.

Capturing groups and named groups make extracted results easier to organize. Named groups use the syntax (?P<name>...) and allow the program to access results through descriptive names.

Example

import re
```

text = """
Order ID: ORD-5842
Customer: Sara
Date: 2026-07-19
Total: $145.75
"""

pattern = re.compile(
r"Order ID:\s*(?PORD-\d+).*?"
r"Customer:\s*(?P[A-Za-z]+).*?"
r"Date:\s*(?P\d{4}-\d{2}-\d{2}).*?"
r"Total:\s*$(?P\d+.\d{2})",
re.DOTALL
)

result = pattern.search(text)

if result:
print("Order:", result.group("order"))
print("Customer:", result.group("customer"))
print("Date:", result.group("date"))
print("Total:", result.group("total"))
```

Output

Order: ORD-5842
```

Customer: Sara
Date: 2026-07-19
Total: 145.75
```

Output Explanation

The named groups capture four important fields. The DOTALL flag allows the lazy period sections to continue across newline characters.

```

29.29 Practical Regex Projects

```

Regular expressions can be used in many practical projects. Examples include form validation, log-file analysis, privacy filtering, document cleanup, search tools, contact extraction, and data-format conversion.

The following example cleans a messy contact note. It extracts phone numbers and email addresses, then hides the phone numbers in the displayed version for privacy.

Example: Contact Information Processor

import re
```

text = """
Contact Sara at [sara@example.com](mailto:sara@example.com) or 416-555-7821.
Michael can be reached at [michael99@mail.ca](mailto:michael99@mail.ca)
or 647-555-9012.
"""

email_pattern = r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+.[A-Za-z]{2,}"
phone_pattern = r"\d{3}-\d{3}-\d{4}"

emails = re.findall(email_pattern, text)
phones = re.findall(phone_pattern, text)

private_text = re.sub(
phone_pattern,
"[PHONE HIDDEN]",
text
)

print("Emails:")
for email in emails:
print("-", email)

print()
print("Phones:")
for phone in phones:
print("-", phone)

print()
print("Private version:")
print(private_text)
```

Output

Emails:
```

* [sara@example.com](mailto:sara@example.com)
* [michael99@mail.ca](mailto:michael99@mail.ca)

Phones:

* 416-555-7821
* 647-555-9012

Private version:

Contact Sara at [sara@example.com](mailto:sara@example.com) or [PHONE HIDDEN].
Michael can be reached at [michael99@mail.ca](mailto:michael99@mail.ca)
or [PHONE HIDDEN].
```

Output Explanation

Two patterns extract the email addresses and phone numbers separately. The sub() function creates a privacy-safe copy by replacing each phone number with a label.

```

29.30 Chapter Practice Exercises

```

These exercises help you practise patterns, raw strings, character classes, quantifiers, groups, lookarounds, matching functions, replacements, flags, validation, and extraction.

  1. Use search() to find the word Python in a sentence.
  2. Find the first number in a string.
  3. Find every number using findall().
  4. Create a pattern for exactly five digits.
  5. Create a pattern for two uppercase letters followed by four digits.
  6. Use a raw string for a digit pattern.
  7. Match a literal period.
  8. Use the period metacharacter to match cat, cot, and cut.
  9. Match vowels using a character class.
  10. Match everything except digits using a negative character class.
  11. Use + to match one or more spaces.
  12. Use ? to make a character optional.
  13. Use braces to require exactly four digits.
  14. Validate a username with anchors.
  15. Create capturing groups for a date.
  16. Create a non-capturing group for two area-code choices.
  17. Use alternation to match three food names.
  18. Create a positive lookahead.
  19. Create a negative lookahead.
  20. Create a positive lookbehind for currency values.
  21. Compare greedy and lazy matching.
  22. Use match() to check the beginning of a string.
  23. Use fullmatch() to validate a postal code.
  24. Use finditer() to display match positions.
  25. Split text using commas, spaces, or semicolons.
  26. Replace repeated spaces with one space.
  27. Hide all phone numbers in a paragraph.
  28. Compile and reuse a product-code pattern.
  29. Use IGNORECASE to find all versions of one word.
  30. Use MULTILINE with beginning anchors.
  31. Create a readable pattern using VERBOSE.
  32. Create a basic email-format validator.
  33. Extract named fields from an order record.

Practice Example: Remove Extra Spaces

import re
```

text = "Python     is   easy   to learn."

clean_text = re.sub(r"\s+", " ", text)

print(clean_text)
```

Output

Python is easy to learn.

Output Explanation

The pattern \s+ matches one or more whitespace characters. Every group of whitespace is replaced with one ordinary space.

```

29.31 Chapter Mini Project

```

Project: Contact Data Validator and Extractor

In this mini project, you will build a program that reads several contact records. It validates names, email addresses, phone numbers, and Canadian postal codes. It also extracts valid information and creates a privacy-safe report.

The project combines compiled patterns, full matching, named groups, replacements, flags, loops, dictionaries, functions, and formatted output. It shows how several small patterns can work together in a practical application.

Complete Program

import re
```

# Compile reusable patterns

name_pattern = re.compile(
r"[A-Za-z]+(?:[ '-][A-Za-z]+)*"
)

email_pattern = re.compile(
r"[A-Za-z0-9._%+-]+"
r"@"
r"[A-Za-z0-9.-]+"
r"."
r"[A-Za-z]{2,}"
)

phone_pattern = re.compile(
r"(?P\d{3})"
r"[- ]"
r"(?P\d{3})"
r"[- ]"
r"(?P\d{4})"
)

postal_pattern = re.compile(
r"[A-Z]\d[A-Z] \d[A-Z]\d",
re.IGNORECASE
)

def validate_name(name):
"""Return True when the complete name follows the pattern."""

```
return bool(name_pattern.fullmatch(name.strip()))
```

def validate_email(email):
"""Return True when the email has a reasonable format."""

```
return bool(email_pattern.fullmatch(email.strip()))
```

def normalize_phone(phone):
"""Validate and convert the phone number to 000-000-0000."""

```
result = phone_pattern.fullmatch(phone.strip())

if not result:
    return None

return (
    f'{result.group("area")}-'
    f'{result.group("exchange")}-'
    f'{result.group("line")}'
)
```

def normalize_postal_code(postal_code):
"""Validate and convert a postal code to uppercase."""

```
cleaned = postal_code.strip().upper()

if postal_pattern.fullmatch(cleaned):
    return cleaned

return None
```

def hide_phone(phone):
"""Hide the middle digits of a normalized phone number."""

```
return re.sub(
    r"(\d{3})-\d{3}-(\d{4})",
    r"\1-***-\2",
    phone
)
```

contacts = [
{
"name": "Sara Farjani",
"email": "[sara@example.com](mailto:sara@example.com)",
"phone": "416-555-7821",
"postal_code": "L4C 5W6"
},
{
"name": "Michael",
"email": "[michael99@mail.ca](mailto:michael99@mail.ca)",
"phone": "647 555 9012",
"postal_code": "m5v 2t6"
},
{
"name": "Ali 123",
"email": "ali-at-example.com",
"phone": "555-88-2211",
"postal_code": "123 456"
},
{
"name": "Mary-Jane Smith",
"email": "[mary.jane@company.org](mailto:mary.jane@company.org)",
"phone": "905-444-3080",
"postal_code": "L3R 1A2"
}
]

valid_contacts = []
invalid_contacts = []

for contact in contacts:
errors = []

```
if not validate_name(contact["name"]):
    errors.append("Invalid name")

if not validate_email(contact["email"]):
    errors.append("Invalid email")

normalized_phone = normalize_phone(contact["phone"])

if normalized_phone is None:
    errors.append("Invalid phone")

normalized_postal = normalize_postal_code(
    contact["postal_code"]
)

if normalized_postal is None:
    errors.append("Invalid postal code")

if errors:
    invalid_contacts.append(
        {
            "contact": contact,
            "errors": errors
        }
    )

else:
    valid_contacts.append(
        {
            "name": contact["name"].strip(),
            "email": contact["email"].strip().lower(),
            "phone": normalized_phone,
            "postal_code": normalized_postal
        }
    )
```

print("CONTACT VALIDATION REPORT")
print("=" * 50)

print()
print("VALID CONTACTS")
print("-" * 50)

if not valid_contacts:
print("No valid contacts were found.")

for number, contact in enumerate(valid_contacts, start=1):
print("Contact", number)
print("Name:", contact["name"])
print("Email:", contact["email"])
print("Phone:", contact["phone"])
print("Postal code:", contact["postal_code"])
print()

print("PRIVACY-SAFE CONTACT LIST")
print("-" * 50)

for contact in valid_contacts:
print(
contact["name"],
"|",
contact["email"],
"|",
hide_phone(contact["phone"]),
"|",
contact["postal_code"]
)

print()
print("INVALID CONTACTS")
print("-" * 50)

if not invalid_contacts:
print("No invalid contacts were found.")

for item in invalid_contacts:
contact = item["contact"]

```
print("Name:", contact["name"])
print("Email:", contact["email"])
print("Phone:", contact["phone"])
print("Postal code:", contact["postal_code"])
print("Errors:", ", ".join(item["errors"]))
print()
```

print("SUMMARY")
print("-" * 50)
print("Total records:", len(contacts))
print("Valid records:", len(valid_contacts))
print("Invalid records:", len(invalid_contacts))
```

Output

CONTACT VALIDATION REPORT
```

==================================================

## VALID CONTACTS

Contact 1
Name: Sara Farjani
Email: [sara@example.com](mailto:sara@example.com)
Phone: 416-555-7821
Postal code: L4C 5W6

Contact 2
Name: Michael
Email: [michael99@mail.ca](mailto:michael99@mail.ca)
Phone: 647-555-9012
Postal code: M5V 2T6

Contact 3
Name: Mary-Jane Smith
Email: [mary.jane@company.org](mailto:mary.jane@company.org)
Phone: 905-444-3080
Postal code: L3R 1A2

## PRIVACY-SAFE CONTACT LIST

Sara Farjani | [sara@example.com](mailto:sara@example.com) | 416-***-7821 | L4C 5W6
Michael | [michael99@mail.ca](mailto:michael99@mail.ca) | 647-***-9012 | M5V 2T6
Mary-Jane Smith | [mary.jane@company.org](mailto:mary.jane@company.org) | 905-***-3080 | L3R 1A2

## INVALID CONTACTS

Name: Ali 123
Email: ali-at-example.com
Phone: 555-88-2211
Postal code: 123 456
Errors: Invalid name, Invalid email, Invalid phone, Invalid postal code

## SUMMARY

Total records: 4
Valid records: 3
Invalid records: 1
```

Project Explanation

The program creates four compiled patterns. The name pattern accepts letters and optional spaces, apostrophes, or hyphens between name sections. The email pattern checks a practical email structure.

The phone pattern uses named capturing groups for the area code, exchange, and final line number. It accepts either spaces or hyphens as separators and returns a normalized hyphenated version.

The postal-code pattern accepts the Canadian letter-digit-letter, space, digit-letter-digit format. The IGNORECASE flag allows lowercase input, while the normalization function converts accepted values to uppercase.

Each contact is tested with all four validation functions. Invalid fields are added to an error list. Contacts with no errors are converted into a consistent format and added to the valid-contact list.

The hide_phone() function uses capturing groups in re.sub(). It keeps the first three and final four digits while replacing the middle section with asterisks.

The final report separates valid and invalid records. It also provides a privacy-safe contact list and summary totals.

How to Run the Mini Project

  1. Open Visual Studio Code, IDLE, PyCharm, or another Python editor.
  2. Create a file named regex_contact_validator.py.
  3. Copy the complete program into the file.
  4. Save the file.
  5. Open a terminal in the same folder.
  6. Run python regex_contact_validator.py.
  7. On some computers, run python3 regex_contact_validator.py.
  8. Review the valid and invalid contact sections.
  9. Add more contact dictionaries and run the program again.
  10. Test different phone-number separators and postal-code letter cases.

Project Challenges

  • Ask the user to enter contacts interactively.
  • Allow optional phone-number parentheses.
  • Allow optional phone extensions.
  • Validate United States ZIP codes.
  • Validate dates in YYYY-MM-DD format.
  • Add a city and province validator.
  • Extract contacts from a large text document.
  • Save valid contacts to a CSV file.
  • Save invalid records to a separate error file.
  • Remove duplicate email addresses.
  • Sort contacts by last name.
  • Create a search tool for names or area codes.
  • Add stronger password-format validation.
  • Create a menu for adding, viewing, and searching contacts.
```
Chapter 2: Development EnvironmentPrepare Your JavaScript Workspace

A modern course built to help learners study step by step with clarity, comfort, and confidence.