Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
108: Groups and Capturing
Up until now, we've used regular expressions mostly to see if a string matches a pattern—a yes or no answer. But in the real world, we usually don't just want to know if a string matches; we want to extract specific pieces of data from it. That's where capturing groups come in.
Let's say we're building a tool to process some messy product data scraped from an old catalog. The data looks like this: PRODUCT: Blue-Suede-Shoes | SKU: 12345-ABC | PRICE: $45.00. We need to pull out the product name, the SKU, and the price into separate Python variables.
Defining our target areas
To capture a specific part of a match, we wrap that part of the regex in parentheses (). I'll start by writing a pattern that matches the whole line but "captures" the three bits of info I actually care about.
import re
data = "PRODUCT: Blue-Suede-Shoes | SKU: 12345-ABC | PRICE: $45.00"
pattern = r"PRODUCT: (.*?) \| SKU: (.*?) \| PRICE: \$(.*)"
match = re.search(pattern, data)
if match:
print("Match found!")
The (.*?) is a non-greedy match. I'm telling Python: "Find the label, then grab everything until you hit the next delimiter." It's a handy trick when you have multiple similar separators in one line.
The Group Zero Trap
Now, I want to get that SKU. I know it's the second set of parentheses, so I'll try to print it. Here is where I usually trip up when I'm rushing—I'll try to access the first group thinking it's the first thing I captured.
# My first (wrong) attempt
print(f"The SKU is: {match.group(0)}")
# Output: The SKU is: PRODUCT: Blue-Suede-Shoes | SKU: 12345-ABC | PRICE: $45.00
Wait, that's the whole string. I forgot that group(0) is always the entire match. The actual capturing groups start at 1. This is a quirk of the re module that catches everyone at least once. Let me fix that.
# Correcting the index
print(f"Product: {match.group(1)}") # Blue-Suede-Shoes
print(f"SKU: {match.group(2)}") # 12345-ABC
print(f"Price: {match.group(3)}") # 45.00
Cleaning it up with Named Groups
Using numbers like group(2) is fine for a small script, but if your regex grows to ten groups, you'll spend half your day counting parentheses. It's a nightmare to maintain. Instead, I prefer named groups using the (?P<name>...) syntax.
It looks a bit more cluttered in the regex string, but it makes the Python code much more readable.
# A more professional approach
pattern = r"PRODUCT: (?P<name>.*?) \| SKU: (?P<sku>.*?) \| PRICE: \$(?P<price>.*)"
match = re.search(pattern, data)
if match:
# Now I can use the name instead of a number
product_name = match.group('name')
sku_code = match.group('sku')
cost = match.group('price')
print(f"Processing {product_name} ({sku_code}) at ${cost}")
By naming the groups, the code becomes self-documenting. If I decide to change the order of the product data later, I don't have to go back and update every single index number in my code.
📋 Practical Task
Exercise: Extracting Log Timestamps and Error Levels
You are analyzing a server log file. Each error line follows this format: [2023-10-12 14:30:05] ERROR: Database connection failed.
Write a Python script that uses named capturing groups to extract the timestamp (the part inside the brackets) and the message (the part after the error level).
Test your solution with the following string: "[2024-01-15 08:12:44] CRITICAL: Disk space low". Your script should print the timestamp and the message as two distinct variables.
There are no comments for now.